Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement graph #16

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open

implement graph #16

wants to merge 21 commits into from

Conversation

AviAvni
Copy link
Contributor

@AviAvni AviAvni commented Oct 1, 2024

Summary by CodeRabbit

  • New Features

    • Introduced a Foreign Function Interface (FFI) for the Graph module, enabling external language interactions.
    • Added new data structures and functions for enhanced graph management, including node and edge operations.
    • Implemented a new CRWLock for improved concurrency control.
    • Added tensor manipulation capabilities with new methods for creating and managing tensors.
    • Enhanced matrix operations with updated function signatures and improved type safety.
    • Introduced DataBlock and DataBlockIterator types for efficient data management.
  • Bug Fixes

    • Resolved issues related to memory management and resource allocation in graph operations.
  • Documentation

    • Enhanced documentation for new structures and functions, improving clarity for end-users.
  • Chores

    • Updated dependencies, including the addition of the parking_lot crate for better mutex management.

Copy link

coderabbitai bot commented Oct 1, 2024

Walkthrough

This pull request introduces significant enhancements to the graph module, focusing on the implementation of a Foreign Function Interface (FFI) for external language interaction. Key changes include the addition of various functions for managing graph entities, improved concurrency control, and refined data structures for matrices and tensors. The introduction of new types and methods facilitates better memory management and operations on nodes, edges, and matrix data. Additionally, a new CRWLock struct is implemented for read-write locking, and several modules are restructured for clarity and maintainability.

Changes

File Path Change Summary
src/graph/ffi.rs Implemented FFI for the Graph module with functions for graph management, including creating graphs, managing nodes and edges, and iterating over tensor ranges.
src/graph/graph.rs Enhanced graph structure with new statistics tracking, concurrency controls, matrix management, and added methods for node and edge operations. Introduced iterators for edges and nodes.
src/graph/matrix/delta_matrix.rs Restructured DeltaMatrix to improve mutex handling and organization. Removed CMutex and introduced Arc<Mutex<()>>. Updated method signatures for better access patterns.
src/graph/matrix/tensor.rs Added Tensor structure with methods for manipulating tensor-like data. Introduced iterators for edge traversal and optimized edge handling methods.
src/binding/crwlock.rs Introduced CRWLock struct for managing read-write locks with methods for acquiring and releasing locks, designed for C and Rust access.
src/graph/matrix/delta_matrix_iter.rs Renamed and updated DeltaMatrixIter constructor to allow specific row boundaries and transpose options. Removed less frequently used methods to simplify the interface.
src/graph/matrix/ffi.rs Updated function signatures to enhance type safety and added GrB_Descriptor for matrix operations. Removed some functions indicating a refactor.
src/binding/graph.rs Changed NodeID, EdgeID, and EntityID types to u64. Introduced new GraphEntity, DataBlock, and DataBlockIterator types with functions for managing data blocks.
Cargo.toml Added dependency for parking_lot crate version 0.12.3.
src/binding/mod.rs Added public module declaration for crwlock.
src/graph/matrix/GraphBLAS.rs Added new constants and modified enums in the GraphBLAS bindings, enhancing matrix and vector operations.

Possibly related PRs

  • implement delta matrix #8: The changes in the DeltaMatrix struct and its associated FFI in this PR are directly related to the main PR's implementation of the Foreign Function Interface (FFI) for the Graph module, as both involve enhancing interoperability with external languages and managing matrix operations.

Suggested reviewers

  • gkorland
  • EmilyMatt
  • swilly22

🐇 In the graph where edges entwine,
New functions added, oh how they shine!
With tensors and locks, our work's now complete,
A dance of data, oh what a treat!
Let's hop through the code, with joy we embrace,
For every new feature, we quicken our pace! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 42

🧹 Outside diff range and nitpick comments (23)
src/graph/mod.rs (1)

6-8: LGTM: Module structure aligns with PR objective

The changes to this file align well with the PR objective of implementing a graph. The addition of the FFI module and making the graph module public are logical steps in this implementation.

A few points to consider:

  1. Ensure that the ffi module is properly implemented and follows best practices for FFI in Rust.
  2. Verify that the public interface of the graph module provides all necessary functionality for graph operations.
  3. Consider adding documentation comments to explain the purpose of each module, especially for the new ffi module.

As the project grows, consider organizing the FFI-related code into a separate top-level module if it's used by multiple components. This can help maintain a clean separation between core logic and FFI bindings.

src/binding/mod.rs (2)

6-7: LGTM! Consider improving module organization and documentation.

The new module declarations for cmutex and crwlock are syntactically correct and follow Rust's naming conventions. These additions suggest an expansion of concurrency-related functionality in the binding.

Consider the following improvements:

  1. Reorder the module declarations alphabetically for better organization:
pub mod cmutex;
pub mod crwlock;
pub mod graph;
  1. Add brief comments explaining the purpose of these new modules. For example:
// Custom mutex implementation
pub mod cmutex;

// Custom read-write lock implementation
pub mod crwlock;

// Graph-related functionality
pub mod graph;

These changes would enhance the readability and maintainability of the code.


Line range hint 1-9: Consider adding module-level documentation.

The file structure is clean and follows common practices for Rust module files. To further improve the code's documentation, consider adding a module-level documentation comment at the beginning of the file, explaining the purpose of the binding module and its contents.

Here's an example of how you could add module-level documentation:

/*
 * Copyright FalkorDB Ltd. 2023 - present
 * Licensed under the Server Side Public License v1 (SSPLv1).
 */

//! The `binding` module provides various bindings and utilities for the FalkorDB project.
//! 
//! This module includes implementations for custom concurrency primitives and graph-related
//! functionality.

pub mod cmutex;
pub mod crwlock;
pub mod graph;

This addition would provide users of the module with a quick overview of its contents and purpose.

src/graph/matrix/mod.rs (1)

6-12: Summary: Expanded public API for graph implementation

The changes in this file consistently make previously private modules (GraphBLAS, delta_matrix_iter, sparse_matrix_iter) public and add a new public tensor module. This expansion of the public API aligns with the PR objective of implementing graph functionality.

While these changes are approved, they represent a significant modification to the module's interface. This could have far-reaching implications for the project's architecture and how it's used by other parts of the codebase or external consumers.

Consider the following recommendations:

  1. Document the newly public modules thoroughly, explaining their purpose and usage.
  2. Update any existing documentation or README files to reflect these API changes.
  3. Consider creating or updating integration tests to ensure the newly public modules work correctly when used from other parts of the codebase.
  4. If this is a breaking change, ensure it's properly communicated in release notes or changelog.
  5. Review the internal structure of each newly public module to ensure it's ready for public consumption (e.g., proper error handling, clear naming conventions, etc.).

Would you like assistance in implementing any of these recommendations?

src/binding/cmutex.rs (3)

10-14: Enhance documentation with safety implications.

The struct definition and its purpose are well-documented. However, it would be beneficial to add more details about the safety implications of using this C mutex wrapper in Rust.

Consider expanding the documentation to include:

  1. Why Rust's native Mutex can't be used in this scenario.
  2. The potential risks associated with using unsafe code and C bindings.
  3. Any precautions users should take when using this struct.

Example:

/// Wrap C mutex as we can't use Rust Mutex in this context.
/// Used to lock the matrix only when we apply pending changes.
///
/// # Safety
/// This struct uses unsafe code to interact with C-style mutexes.
/// Users should ensure proper usage to avoid undefined behavior.
/// Prefer Rust's native `Mutex` when possible for better safety guarantees.
pub struct CMutex {
    mutex: pthread_mutex_t,
}

40-44: Consider logging errors in the Drop implementation.

The Drop implementation correctly calls pthread_mutex_destroy to clean up the mutex. However, it might be beneficial to log any errors that occur during destruction, even though we can't really handle them in Drop.

Consider updating the implementation to log any errors:

impl Drop for CMutex {
    fn drop(&mut self) {
        unsafe {
            let result = libc::pthread_mutex_destroy(&mut self.mutex);
            if result != 0 {
                eprintln!("Failed to destroy mutex: {}", std::io::Error::from_raw_os_error(result));
            }
        }
    }
}

This will print an error message to stderr if the mutex destruction fails, which could be helpful for debugging.


1-44: Overall assessment: Good start, needs safety improvements

The implementation of CMutex as a wrapper for C-style mutexes is a good approach for scenarios where Rust's native Mutex can't be used. However, there are several areas where the implementation can be improved:

  1. Proper use of unsafe: Methods using unsafe C functions should be marked as unsafe.
  2. Error handling: Add error checking and propagation for C function calls.
  3. Documentation: Enhance comments to explain safety implications and usage guidelines.
  4. Method signatures: Consider using &self instead of &mut self for lock and unlock.
  5. Logging: Add error logging in the Drop implementation.

These improvements will enhance the safety, reliability, and usability of the CMutex struct. Once these changes are implemented, the code will be much more robust and align better with Rust's safety principles while still providing the necessary low-level control.

build.rs (1)

Line range hint 21-29: Conditional logic for CLANG looks good, but consider improving portability.

The addition of conditional logic to handle CLANG-specific linker arguments is a good improvement. However, there are a few points to consider:

  1. The hardcoded paths for LLVM libraries might not be portable across different systems. Consider using environment variables or build-time detection for these paths.

  2. The script is linking against both LLVM 17 and 18 libraries. This might lead to compatibility issues or unnecessary dependencies. Consider specifying a single LLVM version or making it configurable.

Consider refactoring the LLVM library path handling for better portability:

if clang == "1" {
    let llvm_lib_path = env::var("LLVM_LIB_PATH").unwrap_or_else(|_| {
        // Default paths, preferably detected at build time
        "/usr/lib/llvm-18/lib".to_string()
    });
    println!("cargo:rustc-link-arg=-L{}", llvm_lib_path);
    println!("cargo:rustc-link-arg=-lomp");
} else {
    println!("cargo:rustc-link-arg=-lgomp");
}

This approach allows for more flexibility and easier configuration across different systems.

src/binding/crwlock.rs (3)

6-14: LGTM! Consider adding more detailed documentation.

The imports and struct definition look good. The use of Box<pthread_rwlock_t> ensures proper heap allocation for FFI.

Consider adding more detailed documentation for the CRWLock struct, including:

  • Why it's needed (inability to use Rust's RWLock)
  • Its thread-safety properties
  • Any limitations or considerations when using it

49-53: Consider adding error checking in Drop implementation.

While pthread_rwlock_destroy shouldn't fail for a valid rwlock, adding a debug_assert! would be consistent with the error checking in other methods. This could help catch potential issues during development.

Consider modifying the drop implementation:

impl Drop for CRWLock {
    fn drop(&mut self) {
        let result = unsafe { libc::pthread_rwlock_destroy(self.rwlock.as_mut()) };
        debug_assert!(result == 0, "pthread_rwlock_destroy failed");
    }
}

This change maintains consistency with the error checking approach used in other methods.


1-53: Overall feedback: Good start, but needs improvements in error handling and API design.

The implementation provides a functional Rust wrapper for pthread_rwlock_t, which is useful for scenarios where Rust's built-in RwLock can't be used. However, there are several areas for improvement:

  1. Error Handling: Implement proper error handling and propagation for all C FFI calls.

  2. API Design: Reconsider the use of &mut self in methods where it might be unnecessarily restrictive.

  3. Documentation: Add comprehensive documentation, including:

    • Explanation of why this wrapper is needed
    • Thread-safety guarantees
    • Potential deadlock scenarios and how to avoid them
    • Best practices for using this rwlock
  4. Consider implementing Send and Sync traits explicitly to clearly define the thread-safety properties of CRWLock.

  5. You might want to add methods for try_read_lock and try_write_lock (wrapping pthread_rwlock_tryrdlock and pthread_rwlock_trywrlock) to provide non-blocking alternatives.

These improvements will make the CRWLock more robust, easier to use correctly, and better integrated with Rust's error handling and concurrency models.

src/graph/matrix/delta_matrix_iter.rs (1)

32-45: LGTM! Consider a minor style improvement.

The new new_range method is well-implemented and provides a useful way to create a DeltaMatrixIter with a specific row range. The implementation is correct and consistent.

Consider simplifying the struct initialization by using the field shorthand notation:

-            min_row: min_row,
-            max_row: max_row,
+            min_row,
+            max_row,
src/graph/matrix/ffi.rs (2)

Line range hint 220-245: Use appropriate error codes for accurate error handling

In Delta_MatrixTupleIter_next_BOOL and Delta_MatrixTupleIter_next_UINT64, returning GrB_Info::GrB_NULL_POINTER for all errors other than Ok(None) may not accurately represent the error conditions. Consider mapping specific errors to corresponding GrB_Info codes for better error reporting.

For example, you might adjust the error handling as follows:

match (*iter).next_bool() {
    Ok(Some((r, c))) => {
        // ...
        GrB_Info::GrB_SUCCESS
    }
    Ok(None) => GrB_Info::GxB_EXHAUSTED,
    Err(e) => {
        match e {
            SpecificErrorType => GrB_Info::GrB_INVALID_OBJECT,
            _ => GrB_Info::GrB_UNKNOWN,
        }
    }
}

Line range hint 15-273: Consolidate #[allow(non_snake_case)] attributes to reduce repetition

The #[allow(non_snake_case)] attribute is applied before each function to suppress naming convention warnings. To reduce code duplication and improve maintainability, consider applying this attribute at the module level or grouping these functions within a scope where the attribute applies to all of them.

You can place the attribute at the beginning of the file or module:

#![allow(non_snake_case)]
src/graph/ffi.rs (3)

37-39: Consider adding safety documentation for Graph_AcquireReadLock.

The function Graph_AcquireReadLock uses unsafe code and operates on raw pointers. It's important to document the safety requirements and assumptions for this function to ensure correct usage by callers.

Consider adding comments explaining the safety requirements for g:

/// Acquires a read lock on the graph.
/// 
/// # Safety
/// 
/// - `g` must be a valid, non-null pointer to a `Graph` instance.
/// - The graph must be properly initialized before calling this function.
unsafe extern "C" fn Graph_AcquireReadLock(g: *mut Graph) {
    (&mut *g).acquire_read_lock();
}

43-45: Consider adding safety documentation for Graph_AcquireWriteLock.

Similar to Graph_AcquireReadLock, it's important to document the safety requirements for Graph_AcquireWriteLock.

Add safety comments for g:

/// Acquires a write lock on the graph.
/// 
/// # Safety
/// 
/// - `g` must be a valid, non-null pointer to a `Graph` instance.
/// - The graph must be properly initialized before calling this function.
unsafe extern "C" fn Graph_AcquireWriteLock(g: *mut Graph) {
    (&mut *g).acquire_write_lock();
}

50-51: Consider adding safety documentation for Graph_ReleaseLock.

Ensure that the safety considerations for Graph_ReleaseLock are documented.

Include safety comments:

/// Releases the lock on the graph.
/// 
/// # Safety
/// 
/// - `g` must be a valid, non-null pointer to a `Graph` instance that was previously locked.
unsafe extern "C" fn Graph_ReleaseLock(g: *mut Graph) {
    (&mut *g).release_lock();
}
src/graph/matrix/tensor.rs (5)

24-26: Add documentation for the single_edge function.

The function single_edge checks if the most significant bit (MSB) of current_edge is not set. Adding a doc comment explaining its purpose and how it relates to edge representation would improve code readability and maintainability.


28-30: Add documentation for the set_msb function.

The set_msb function sets the most significant bit of an EdgeID. Providing a doc comment would help clarify its role in marking edge representations.


32-34: Add documentation for the clear_msb function.

The clear_msb function clears the most significant bit of an EdgeID. Including a doc comment explaining its purpose would enhance code clarity.


36-38: Add documentation comments for the Tensor struct.

Adding Rust doc comments to the Tensor struct will improve code readability and help other developers understand its purpose and usage.


40-283: Add documentation comments for public methods in Tensor implementation.

Adding Rust doc comments (///) to the public methods of the Tensor struct, such as set_element, set_elements, remove_elements, iter, and others, will improve code readability and help users understand how to use these methods.

src/graph/matrix/GraphBLAS.rs (1)

Line range hint 1-3: Ensure safe exposure of __BindgenComplex types

The types GxB_FC32_t and GxB_FC64_t are being publicly defined as aliases for __BindgenComplex<f32> and __BindgenComplex<f64>, respectively. Since __BindgenComplex is generated by bindgen and primarily intended for internal FFI use, exposing it publicly may lead to unintended usage or safety issues.

Consider wrapping these complex types in safer, more idiomatic Rust abstractions or providing a safe interface for users to interact with complex numbers.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 8a82588 and 1cca947.

📒 Files selected for processing (16)
  • build.rs (1 hunks)
  • src/binding/cmutex.rs (1 hunks)
  • src/binding/crwlock.rs (1 hunks)
  • src/binding/graph.rs (4 hunks)
  • src/binding/mod.rs (1 hunks)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
  • src/graph/matrix/GraphBLAS.rs (1 hunks)
  • src/graph/matrix/delta_matrix.rs (5 hunks)
  • src/graph/matrix/delta_matrix_iter.rs (1 hunks)
  • src/graph/matrix/ffi.rs (8 hunks)
  • src/graph/matrix/mod.rs (1 hunks)
  • src/graph/matrix/tensor.rs (1 hunks)
  • src/graph/mod.rs (1 hunks)
  • src/lib.rs (1 hunks)
  • src/undo_log/undo_log.rs (2 hunks)
🔇 Additional comments (26)
src/graph/mod.rs (1)

7-7: Graph module made public

The graph module is now declared as public (pub mod graph;). This change aligns with the PR objective of implementing a graph, as it makes the graph functionality accessible to other parts of the codebase.

Let's verify the contents and usage of the public graph module:

#!/bin/bash
# Description: Check the contents and usage of the public graph module

# Test 1: Check if the graph.rs file exists
echo "Checking for graph.rs file:"
fd --type f "graph.rs" src/graph

# Test 2: Look for public functions or structs in the graph module
echo "Searching for public declarations in the graph module:"
rg --type rust "pub (fn|struct|enum)" src/graph/graph.rs

# Test 3: Check for usage of the graph module in other files
echo "Checking for usage of the graph module:"
rg --type rust "use (crate::)?graph::graph::" src
src/graph/matrix/mod.rs (3)

11-12: Approved: sparse_matrix_iter made public and new tensor module added

The change to make sparse_matrix_iter public aligns with the previous changes and likely supports the graph implementation. The addition of a new public tensor module suggests new functionality related to tensors.

To ensure these changes don't unintentionally expose internal implementation details or break existing code, please run the following verification script:

Could you please provide more information about the new tensor module? What functionality does it provide, and how does it relate to the graph implementation?

✅ Verification successful

Verified: Changes to sparse_matrix_iter and addition of tensor module are appropriate

The sparse_matrix_iter module has been correctly made public, and the new tensor module is well-implemented to support tensor operations within the graph matrix. No unintended exposures or issues were found based on the verification results.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any existing usages of sparse_matrix_iter and verify the new tensor module

# Test: Search for sparse_matrix_iter usages
echo "Searching for sparse_matrix_iter usages:"
rg --type rust "use .*sparse_matrix_iter" -g '!src/graph/matrix/mod.rs'

# Test: Check if there are any visibility modifiers on items within sparse_matrix_iter
echo "Checking for visibility modifiers within sparse_matrix_iter:"
rg --type rust "pub (struct|enum|fn|trait|type)" src/graph/matrix/sparse_matrix_iter.rs

# Test: Verify the contents of the new tensor module
echo "Checking the contents of the new tensor module:"
cat src/graph/matrix/tensor.rs

Length of output: 15669


6-6: Approved: GraphBLAS module made public

The change to make the GraphBLAS module public aligns with the PR objective of implementing a graph functionality. This will allow other parts of the codebase to access GraphBLAS features.

To ensure this change doesn't unintentionally expose internal implementation details or break existing code, please run the following verification script:

✅ Verification successful

Verified: GraphBLAS module is correctly made public

The GraphBLAS module has been successfully made public without introducing any issues. Existing usages in sparse_matrix.rs are consistent with this change, and all necessary components within GraphBLAS.rs have appropriate visibility modifiers.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any existing usages of GraphBLAS that might be affected by this change

# Test: Search for GraphBLAS usages
echo "Searching for GraphBLAS usages:"
rg --type rust "use .*GraphBLAS" -g '!src/graph/matrix/mod.rs'

# Test: Check if there are any visibility modifiers on items within GraphBLAS
echo "Checking for visibility modifiers within GraphBLAS:"
rg --type rust "pub (struct|enum|fn|trait|type)" src/graph/matrix/GraphBLAS.rs

Length of output: 36552


8-8: Approved: delta_matrix_iter module made public

Making the delta_matrix_iter module public is likely necessary for implementing graph functionality, possibly for iterating over delta matrices in other parts of the codebase.

To ensure this change doesn't unintentionally expose internal implementation details or break existing code, please run the following verification script:

✅ Verification successful

Action Required: Correct Verification Scripts

The previous verification scripts mistakenly referenced sparse_matrix_iter instead of delta_matrix_iter. Please use the corrected scripts below to ensure accurate verification of the delta_matrix_iter module:


Verified: delta_matrix_iter Module Public Access

The delta_matrix_iter module has been successfully made public without affecting other parts of the codebase. No external usages were found, and all intended items within the module are appropriately exposed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any existing usages of delta_matrix_iter that might be affected by this change

# Test: Search for delta_matrix_iter usages
echo "Searching for delta_matrix_iter usages:"
rg --type rust "use .*delta_matrix_iter" -g '!src/graph/matrix/mod.rs'

# Test: Check if there are any visibility modifiers on items within delta_matrix_iter
echo "Checking for visibility modifiers within delta_matrix_iter:"
rg --type rust "pub (struct|enum|fn|trait|type)" src/graph/matrix/delta_matrix_iter.rs

Length of output: 15669


Script:

#!/bin/bash
# Description: Check for any existing usages of delta_matrix_iter that might be affected by this change

# Test: Search for delta_matrix_iter usages
echo "Searching for delta_matrix_iter usages:"
rg --type rust "use .*delta_matrix_iter" -g '!src/graph/matrix/mod.rs'

# Test: Check if there are any visibility modifiers on items within delta_matrix_iter
rg --type rust "pub (struct|enum|fn|trait|type)" src/graph/matrix/delta_matrix_iter.rs

Length of output: 684

src/binding/cmutex.rs (1)

1-9: LGTM: Appropriate file header and imports.

The file header contains the necessary copyright and license information. The imports from the standard library and libc are relevant for the implementation of the C-style mutex wrapper.

build.rs (3)

1-4: LGTM: Copyright notice added.

The addition of the copyright notice is a good practice. It clearly states the ownership and licensing terms of the code.


11-11: LGTM: CLANG environment variable check added.

The addition of the CLANG environment variable check is consistent with the existing checks and ensures the build script reruns when necessary.


Line range hint 1-32: Overall, the changes look good with some suggestions for improvement.

The modifications to build.rs enhance the build script's flexibility for different compiler environments, particularly with the addition of CLANG-specific handling. The copyright notice and environment variable checks are well-implemented.

The main area for potential improvement is in the handling of LLVM library paths, which could be made more portable and configurable. Consider implementing the suggestions provided earlier to enhance the script's adaptability across different systems.

These changes align well with the PR objective of implementing a graph, as they seem to be setting up the necessary build environment for the graph implementation.

src/binding/crwlock.rs (2)

41-46: ⚠️ Potential issue

Improve error handling and consider method signature for release.

  1. Similar to the other methods, release should handle errors properly and propagate them to the caller.

  2. The method takes &mut self, which might be unnecessarily restrictive. Unlocking a rwlock doesn't require exclusive access to the lock itself.

Suggested improvements:

  1. Error handling:
pub fn release(&self) -> Result<(), std::io::Error> {
    let result = unsafe { libc::pthread_rwlock_unlock(self.rwlock.as_ref() as *const _ as *mut _) };
    if result != 0 {
        Err(std::io::Error::from_raw_os_error(result))
    } else {
        Ok(())
    }
}
  1. Change the method signature to &self instead of &mut self.

To ensure that changing to &self doesn't break any existing code, let's search for uses of release:

rg "\.release\(\)" --type rust

27-32: ⚠️ Potential issue

Improve error handling and consider method signature.

  1. The current implementation uses debug_assert!, which only checks in debug builds. Consider handling errors properly and propagating them to the caller.

  2. The method takes &mut self, which is more restrictive than necessary for a read lock. This prevents multiple read locks from being acquired simultaneously, which is typically allowed for reader-writer locks.

Suggested improvements:

  1. Error handling:
pub fn acquire_read(&self) -> Result<(), std::io::Error> {
    let result = unsafe { libc::pthread_rwlock_rdlock(self.rwlock.as_ref() as *const _ as *mut _) };
    if result != 0 {
        Err(std::io::Error::from_raw_os_error(result))
    } else {
        Ok(())
    }
}
  1. Change the method signature to &self instead of &mut self to allow multiple simultaneous read locks.

To ensure that changing to &self doesn't break any existing code, let's search for uses of acquire_read:

✅ Verification successful

Verification Successful: Safe to Refactor acquire_read Method Signature

The acquire_read method is only invoked with mutable references (&mut self) in the codebase. Changing the method signature to &self will not disrupt existing functionalities and will enable multiple simultaneous read locks as intended.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

rg "acquire_read" --type rust

Length of output: 82


Script:

rg "acquire_read" --type rust

Length of output: 257


Script:

rg "acquire_read" --type rust -A 2

Length of output: 486

src/lib.rs (1)

15-15: LGTM: Addition of RedisModule_Realloc function

The addition of the RedisModule_Realloc function complements the existing memory management functions and allows for resizing of allocated memory. The Option<...> wrapper is consistent with other Redis module functions, allowing for graceful handling if the function is not available.

Suggestion: Implement realloc method in FalkorDBAlloc

Consider implementing a realloc method in the FalkorDBAlloc struct to utilize this new function. This would provide a complete set of memory management operations (alloc, realloc, dealloc) for the allocator.

Here's a suggested implementation:

unsafe impl GlobalAlloc for FalkorDBAlloc {
    // ... existing alloc and dealloc methods ...

    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
        match RedisModule_Realloc {
            Some(realloc) => realloc(ptr.cast(), new_size).cast(),
            None => panic!("realloc function not found"),
        }
    }
}

Query: Relevance to PR objective

The PR objective mentions implementing a graph, but this change seems unrelated to that goal. Could you clarify how this memory management enhancement contributes to the graph implementation?

src/graph/matrix/ffi.rs (5)

Line range hint 17-24: Function Delta_Matrix_new implementation is correct

The function correctly allocates a new DeltaMatrix and initializes it with the provided parameters.


Line range hint 64-71: Function Delta_Matrix_setElement_BOOL implementation looks good

The function correctly sets a boolean element in the matrix at the specified indices and returns the appropriate success code.


93-100: Function Delta_mxm implementation is appropriate

The matrix multiplication function correctly calls mxm on the DeltaMatrix and handles the pointers safely.


105-112: Function Delta_eWiseAdd implementation is correct

The element-wise addition function properly invokes element_wise_add and manages the inputs and outputs correctly.


124-129: Function Delta_Matrix_copy implementation is accurate

The function correctly copies the contents of one DeltaMatrix to another.

src/binding/graph.rs (3)

8-8: Import statement is appropriate

The use statement correctly imports the Graph struct from crate::graph::graph, enabling access to the Graph type within this module.


62-68: Addition of GraphEntity struct

The GraphEntity struct provides a general representation for graph entities, mirroring the structure of the existing Node struct. This addition enhances code clarity and promotes reusability.


10-12: Verify the impact of changing ID types to u64

Changing NodeID, EdgeID, and EntityID from i64 to u64 ensures that identifiers are non-negative, which aligns with typical usage. However, ensure that all parts of the codebase that use these types are updated accordingly, and that there are no dependencies on negative values.

Run the following script to identify potential issues:

✅ Verification successful

Impact verification successful: No issues found with changing ID types to u64. All usages are updated accordingly, and no dependencies on negative values detected.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find usages of NodeID, EdgeID, and EntityID that may assume signed integers.

# Search for any casts or operations that could be affected by the change.
rg --type rust --word-regexp "(NodeID|EdgeID|EntityID)" --context 3

Length of output: 12310

src/undo_log/undo_log.rs (1)

268-272: Verify the logic of setting id to u64::MAX and associated assertions

In the rollback process for deleted nodes and edges, the id fields are set to u64::MAX:

  • Line 268: id: u64::MAX,
  • Line 280: id: u64::MAX,

Then, assertions are made:

  • Line 270: debug_assert!(*node_id >= node.id);
  • Line 288: debug_assert!(*edge_id >= edge.id);

Since u64::MAX is the maximum possible value, these assertions will likely fail unless *node_id or *edge_id are also u64::MAX. Please verify that this logic achieves the intended behavior and adjust if necessary.

Also applies to: 280-288

src/graph/ffi.rs (1)

227-232: Possible incorrect deletion check in Graph_EntityIsDeleted.

The function checks if attributes is null and returns false if it is. This may not correctly represent the deletion status of the entity.

Please verify that the logic for checking if a GraphEntity is deleted is correct. The current implementation may not accurately reflect the deletion status.

src/graph/matrix/delta_matrix.rs (3)

8-11: Ensure the correct module path for CMutex import

The code imports CMutex from crate::binding::cmutex::CMutex. Verify that the cmutex module exists under crate::binding and contains the CMutex struct to ensure the code compiles correctly.


103-105: Addition of m_mut method for mutable access

The m_mut method provides mutable access to the matrix field, which enhances flexibility when modifying the matrix. Ensure that mutable access aligns with the intended design and concurrency considerations are addressed.


559-559: Variable a shadows previous declaration in test_new_matrix

As above, the variable a is redeclared at line 559. This shadowing can be confusing.

src/graph/matrix/GraphBLAS.rs (2)

365-367: Approved: Enum and attribute additions are correct

The additions of the #[repr(i32)] and derivations for GrB_Info, as well as new enum variants, are appropriate and correctly implemented. These changes ensure that the Rust enum correctly matches the underlying C representation and derives standard traits for usability.


364-364: Verify the correctness of GrB_INDEX_MAX value

The constant GrB_INDEX_MAX is defined as:

pub const GrB_INDEX_MAX: GrB_Index = (1u64 << 60) - 1;

Please confirm that this value accurately reflects the maximum index as defined in the GraphBLAS C API. Ensuring consistency with the original C definition is crucial for correct interoperability.

You can run the following script to compare the value with the GraphBLAS C header files:

This script searches for the definition of GrB_INDEX_MAX in C header files to ensure consistency.

@@ -3,4 +3,6 @@
* Licensed under the Server Side Public License v1 (SSPLv1).
*/

mod ffi;
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

FFI module defined but not utilized

The ffi.rs file exists and contains FFI-related function declarations. However, no usages of the ffi module were found in the codebase, which may indicate unused or incomplete integration.

🔗 Analysis chain

New FFI module added

The addition of mod ffi; introduces a new module for Foreign Function Interface. This suggests that the graph implementation might involve interactions with code written in other languages.

Let's verify the contents and usage of this new FFI module:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the contents and usage of the new FFI module

# Test 1: Check if the ffi.rs file exists
echo "Checking for ffi.rs file:"
fd --type f "ffi.rs" src/graph

# Test 2: Look for FFI-related function declarations
echo "Searching for FFI-related function declarations:"
rg --type rust "extern \"C\"" src/graph/ffi.rs

# Test 3: Check for usage of the FFI module in other files
echo "Checking for usage of the FFI module:"
rg --type rust "use (crate::)?graph::ffi" src

Length of output: 2659


Script:

#!/bin/bash
# Description: Corrected verification of the new FFI module

# Test 1: Check if the ffi.rs file exists
echo "Checking for ffi.rs file:"
fd --type f "ffi.rs" src/graph

# Test 2: Look for FFI-related function declarations
echo "Searching for FFI-related function declarations in ffi.rs:"
rg --type rust 'extern "C"' src/graph/ffi.rs

# Test 3: Check for usage of the FFI module in other files
echo "Checking for usage of the FFI module:"
rg --type rust 'use (crate::)?graph::ffi' src

Length of output: 3664

Comment on lines 17 to 25
pub fn new() -> Self {
unsafe {
let mut mutex = MaybeUninit::uninit();
libc::pthread_mutex_init(mutex.as_mut_ptr(), null_mut());
Self {
mutex: mutex.assume_init(),
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety and error handling in the new method.

The new method correctly initializes the mutex, but there are a few improvements that can be made:

  1. The method should be marked as unsafe due to the use of unsafe operations.
  2. Error handling should be added for the pthread_mutex_init call.
  3. Consider using a const pointer for the mutex attributes instead of null_mut().

Here's a suggested implementation:

pub unsafe fn new() -> Result<Self, std::io::Error> {
    let mut mutex = MaybeUninit::uninit();
    let result = libc::pthread_mutex_init(mutex.as_mut_ptr(), std::ptr::null());
    if result != 0 {
        return Err(std::io::Error::from_raw_os_error(result));
    }
    Ok(Self {
        mutex: mutex.assume_init(),
    })
}

This implementation:

  • Marks the function as unsafe.
  • Returns a Result to handle potential errors.
  • Uses std::ptr::null() instead of null_mut() for the attributes pointer.
  • Checks the return value of pthread_mutex_init and returns an error if initialization fails.

Comment on lines 27 to 37
pub fn lock(&mut self) {
unsafe {
libc::pthread_mutex_lock(&mut self.mutex);
}
}

pub fn unlock(&mut self) {
unsafe {
libc::pthread_mutex_unlock(&mut self.mutex);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety and error handling in lock and unlock methods.

The lock and unlock methods need several improvements:

  1. Both methods should be marked as unsafe due to the use of unsafe C functions.
  2. Error handling should be added for both pthread_mutex_lock and pthread_mutex_unlock calls.
  3. Consider changing the methods to take &self instead of &mut self to allow multiple locks, which is typical for mutex behavior.

Here's a suggested implementation for both methods:

pub unsafe fn lock(&self) -> Result<(), std::io::Error> {
    let result = libc::pthread_mutex_lock(self.mutex.as_ptr());
    if result != 0 {
        return Err(std::io::Error::from_raw_os_error(result));
    }
    Ok(())
}

pub unsafe fn unlock(&self) -> Result<(), std::io::Error> {
    let result = libc::pthread_mutex_unlock(self.mutex.as_ptr());
    if result != 0 {
        return Err(std::io::Error::from_raw_os_error(result));
    }
    Ok(())
}

These implementations:

  • Mark the functions as unsafe.
  • Return a Result to handle potential errors.
  • Take &self instead of &mut self.
  • Check the return value of the pthread functions and return an error if the operation fails.

Comment on lines 17 to 25
pub fn new() -> Self {
let mut res = CRWLock {
rwlock: Box::new(PTHREAD_RWLOCK_INITIALIZER),
};
unsafe {
libc::pthread_rwlock_init(res.rwlock.as_mut(), null_mut());
}
res
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for pthread_rwlock_init.

The initialization of the rwlock looks correct, but there's no error handling for pthread_rwlock_init. This function can fail in certain conditions (e.g., insufficient memory).

Consider adding error handling:

pub fn new() -> Result<Self, std::io::Error> {
    let mut rwlock = Box::new(PTHREAD_RWLOCK_INITIALIZER);
    let result = unsafe {
        libc::pthread_rwlock_init(rwlock.as_mut(), null_mut())
    };
    if result != 0 {
        Err(std::io::Error::last_os_error())
    } else {
        Ok(CRWLock { rwlock })
    }
}

This change would propagate any initialization errors to the caller.

Comment on lines 34 to 39
pub fn acquire_write(&mut self) {
unsafe {
let res = libc::pthread_rwlock_wrlock(self.rwlock.as_mut());
debug_assert!(res == 0, "pthread_rwlock_wrlock failed");
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve error handling for acquire_write.

Similar to acquire_read, this method should handle errors properly and propagate them to the caller. The use of debug_assert! only checks in debug builds.

Suggested improvement:

pub fn acquire_write(&mut self) -> Result<(), std::io::Error> {
    let result = unsafe { libc::pthread_rwlock_wrlock(self.rwlock.as_mut()) };
    if result != 0 {
        Err(std::io::Error::from_raw_os_error(result))
    } else {
        Ok(())
    }
}

This change ensures that any errors during lock acquisition are properly handled and communicated to the caller.

Comment on lines 759 to 770
let m = self.get_node_label_matrix();

let mut it = DeltaMatrixIter::new_range(m, n.id, n.id);
let mut i = 0;

while let Ok(Some((_, j))) = it.next_bool() {
labels[i as usize] = j as i32;
i += 1;
}

i
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent potential buffer overflow in get_node_labels

In the get_node_labels function, there is no check to ensure that the labels slice has sufficient capacity to store all labels of the node. If a node has more labels than the length of the provided labels slice, this could lead to an out-of-bounds write.

Consider modifying the function to return a Vec<i32> containing the labels, or ensure that the labels slice is sized appropriately before calling the function.

Comment on lines 928 to 948
fn optimized_single_edge_form_connection(
&mut self,
src: u64,
dest: u64,
edge_id: u64,
r: i32,
) {
let m = self.get_relation_matrix(r, false);
m.m_mut().set_element_u64(edge_id, src, dest);
m.transposed_mut()
.unwrap()
.m_mut()
.set_element_bool(true, dest, src);

let adj = self.get_adjacency_matrix(false);
adj.m_mut().set_element_bool(true, src, dest);
adj.transposed_mut()
.unwrap()
.m_mut()
.set_element_bool(true, dest, src);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor duplicated code in optimized_single_edge_form_connection

The method optimized_single_edge_form_connection contains duplicated code for setting elements in matrices and their transposes. Refactoring this code can reduce duplication and improve maintainability.

Consider extracting the repeated logic into a helper function to avoid duplication:

fn set_matrix_elements(
    matrix: &mut DeltaMatrix,
    src: u64,
    dest: u64,
    value: impl Into<GrB_Value>,
) {
    matrix.m_mut().set_element(value.into(), src, dest);
    matrix
        .transposed_mut()
        .unwrap()
        .m_mut()
        .set_element(value.into(), dest, src);
}

// Then use the helper function:
set_matrix_elements(&mut self.relations[r as usize], src, dest, edge_id);
set_matrix_elements(&mut self.adjacency_matrix, src, dest, true);

Comment on lines +961 to +963
unsafe { GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, node_count) };

let mut v = unsafe { v.assume_init() };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check return value of GrB_Vector_new to handle potential errors

In the set_node_labels function, the call to GrB_Vector_new does not check for errors. Since this function can fail, not checking the return value may lead to undefined behavior.

Modify the code to check the return value and handle errors appropriately:

 unsafe { 
-    GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, node_count);
+    let info = GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, node_count);
+    grb_check!(info);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
unsafe { GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, node_count) };
let mut v = unsafe { v.assume_init() };
unsafe {
let info = GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, node_count);
grb_check!(info);
}
let mut v = unsafe { v.assume_init() };

Comment on lines 417 to 423
debug_assert!({
let mut v = Vec::new();
nodes.iter().all(|n| {
self.get_node_edges(n, GraphEdgeDir::Both, -1, &mut v);
v.is_empty()
})
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix accumulation issue in debug_assert! by clearing vector v

In the delete_nodes function, within the debug_assert!, the vector v is not cleared between iterations. This causes v to accumulate edges from previous nodes, leading to incorrect assertion behavior when checking v.is_empty().

Apply this diff to clear v at the beginning of each iteration:

 debug_assert!({
     let mut v = Vec::new();
     nodes.iter().all(|n| {
+        v.clear();
         self.get_node_edges(n, GraphEdgeDir::Both, -1, &mut v);
         v.is_empty()
     })
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
debug_assert!({
let mut v = Vec::new();
nodes.iter().all(|n| {
self.get_node_edges(n, GraphEdgeDir::Both, -1, &mut v);
v.is_empty()
})
});
debug_assert!({
let mut v = Vec::new();
nodes.iter().all(|n| {
v.clear();
self.get_node_edges(n, GraphEdgeDir::Both, -1, &mut v);
v.is_empty()
})
});

Comment on lines 117 to 154
pub fn new(
node_cap: u64,
edge_cap: u64,
) -> Self {
let nodes = unsafe {
DataBlock_New(
node_cap,
node_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
let edges = unsafe {
DataBlock_New(
edge_cap,
edge_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
Self {
reserved_node_count: 0,
nodes: nodes,
edges: edges,
adjacency_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, true),
labels: Vec::new(),
node_labels: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
relations: Vec::new(),
zero_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
rwlock: CRWLock::new(),
writelocked: false,
matrix_policy: MatrixPolicy::FlushResize,
stats: GraphStatistics {
node_count: Vec::new(),
edge_count: Vec::new(),
},
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential null pointers in Graph::new

In the Graph::new function, the calls to DataBlock_New are unsafe and may return null pointers if the allocation fails. Dereferencing these null pointers can lead to undefined behavior.

Add checks to ensure that nodes and edges are not null before proceeding:

 let nodes = unsafe { DataBlock_New(...) };
+assert!(!nodes.is_null(), "Failed to allocate nodes DataBlock");

 let edges = unsafe { DataBlock_New(...) };
+assert!(!edges.is_null(), "Failed to allocate edges DataBlock");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn new(
node_cap: u64,
edge_cap: u64,
) -> Self {
let nodes = unsafe {
DataBlock_New(
node_cap,
node_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
let edges = unsafe {
DataBlock_New(
edge_cap,
edge_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
Self {
reserved_node_count: 0,
nodes: nodes,
edges: edges,
adjacency_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, true),
labels: Vec::new(),
node_labels: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
relations: Vec::new(),
zero_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
rwlock: CRWLock::new(),
writelocked: false,
matrix_policy: MatrixPolicy::FlushResize,
stats: GraphStatistics {
node_count: Vec::new(),
edge_count: Vec::new(),
},
}
}
pub fn new(
node_cap: u64,
edge_cap: u64,
) -> Self {
let nodes = unsafe {
DataBlock_New(
node_cap,
node_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
assert!(!nodes.is_null(), "Failed to allocate nodes DataBlock");
let edges = unsafe {
DataBlock_New(
edge_cap,
edge_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
assert!(!edges.is_null(), "Failed to allocate edges DataBlock");
Self {
reserved_node_count: 0,
nodes: nodes,
edges: edges,
adjacency_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, true),
labels: Vec::new(),
node_labels: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
relations: Vec::new(),
zero_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
rwlock: CRWLock::new(),
writelocked: false,
matrix_policy: MatrixPolicy::FlushResize,
stats: GraphStatistics {
node_count: Vec::new(),
edge_count: Vec::new(),
},
}
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Outside diff range and nitpick comments (4)
src/graph/graph.rs (4)

102-116: Add documentation for the Graph struct and its fields

The Graph struct is a central part of this module, but it lacks documentation. Adding clear and concise documentation for the struct and its fields would greatly improve code readability and maintainability.

Consider adding documentation comments to the Graph struct and its fields. For example:

/// Represents a graph data structure with nodes, edges, and their relationships.
pub struct Graph {
    /// The number of reserved nodes.
    reserved_node_count: i32,
    /// Pointer to the data block containing nodes.
    nodes: *mut DataBlock,
    /// Pointer to the data block containing edges.
    edges: *mut DataBlock,
    /// The adjacency matrix representing connections between nodes.
    adjacency_matrix: DeltaMatrix,
    /// Vector of matrices representing node labels.
    labels: Vec<DeltaMatrix>,
    /// Matrix representing the labels of each node.
    node_labels: DeltaMatrix,
    /// Vector of tensors representing relationships between nodes.
    relations: Vec<Tensor>,
    // ... add comments for other fields
}

Adding these comments will help other developers (including your future self) understand the purpose and usage of each field in the Graph struct.


1070-1240: Consider a safer approach for test initialization

The current test initialization uses unsafe code to reset allocations and set global options. This could lead to undefined behavior if not used correctly.

Consider creating safe wrapper functions for the initialization steps:

fn safe_init() -> Result<(), &'static str> {
    unsafe {
        Alloc_Reset();
        if GrB_init(GrB_Mode::GrB_NONBLOCKING) != GrB_Info::GrB_SUCCESS {
            return Err("Failed to initialize GraphBLAS");
        }
        if GxB_Global_Option_set(GxB_Option_Field::GxB_FORMAT, GxB_Format_Value::GxB_BY_ROW) != GrB_Info::GrB_SUCCESS {
            return Err("Failed to set global option");
        }
        if Config_Option_set(
            ConfigOptionField::DELTA_MAX_PENDING_CHANGES,
            "10000\0".as_ptr() as *const c_char,
            null_mut(),
        ) != 0 {
            return Err("Failed to set config option");
        }
    }
    Ok(())
}

// Then in each test:
#[test]
fn test_new_graph() -> Result<(), &'static str> {
    safe_init()?;
    // ... rest of the test ...
    Ok(())
}

This approach provides a safer interface for test initialization and allows for proper error handling.


1070-1240: Add tests for error cases

The current test suite focuses on successful operations but doesn't cover error cases. Adding tests for error scenarios would improve the robustness of the code.

Consider adding tests for error cases, such as:

  1. Creating a graph with invalid dimensions
  2. Adding an edge between non-existent nodes
  3. Deleting a node that has edges
  4. Accessing a non-existent node or edge

For example:

#[test]
fn test_create_edge_with_invalid_nodes() {
    let mut g = Graph::new(5, 5);
    let mut edge = Edge::default();
    assert!(g.create_edge(10, 11, 0, &mut edge).is_err());
}

#[test]
fn test_delete_node_with_edges() {
    let mut g = Graph::new(5, 5);
    let mut n1 = g.reserve_node();
    let mut n2 = g.reserve_node();
    g.create_node(&mut n1, &[]);
    g.create_node(&mut n2, &[]);
    let r = g.add_relation_type();
    let mut edge = Edge::default();
    g.create_edge(n1.id, n2.id, r, &mut edge);
    assert!(g.delete_nodes(&[n1]).is_err());
}

These additional tests will help ensure that the Graph struct handles error cases correctly.


1070-1240: Add tests for uncovered public methods

Some public methods of the Graph struct are not covered by the current test suite. Adding tests for these methods would improve code coverage and ensure their correct functionality.

Consider adding tests for the following methods:

  1. get_node_labels
  2. get_node_degree
  3. get_edges_connecting_nodes
  4. set_node_labels
  5. relationship_contains_multi_edge

For example:

#[test]
fn test_get_node_labels() {
    let mut g = Graph::new(5, 5);
    let mut node = g.reserve_node();
    let label1 = g.add_label();
    let label2 = g.add_label();
    g.create_node(&mut node, &[label1, label2]);
    let mut labels = vec![0; 2];
    let count = g.get_node_labels(&node, &mut labels);
    assert_eq!(count, 2);
    assert_eq!(labels, vec![label1, label2]);
}

#[test]
fn test_get_node_degree() {
    let mut g = Graph::new(5, 5);
    let mut n1 = g.reserve_node();
    let mut n2 = g.reserve_node();
    g.create_node(&mut n1, &[]);
    g.create_node(&mut n2, &[]);
    let r = g.add_relation_type();
    let mut edge = Edge::default();
    g.create_edge(n1.id, n2.id, r, &mut edge);
    assert_eq!(g.get_node_degree(&n1, GraphEdgeDir::Outgoing, -1), 1);
    assert_eq!(g.get_node_degree(&n2, GraphEdgeDir::Incoming, -1), 1);
}

These additional tests will help ensure that all public methods of the Graph struct are working correctly.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 1cca947 and 7131eb6.

📒 Files selected for processing (3)
  • src/binding/graph.rs (4 hunks)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/binding/graph.rs
  • src/graph/ffi.rs

Comment on lines +119 to +130
pub fn new(
node_cap: u64,
edge_cap: u64,
) -> Self {
let nodes = unsafe {
DataBlock_New(
node_cap,
node_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for DataBlock_New calls

The Graph::new method uses unsafe calls to DataBlock_New without checking if the returned pointers are null. This could lead to undefined behavior if the allocation fails.

Consider adding null checks and error handling for the DataBlock_New calls:

let nodes = unsafe {
    let ptr = DataBlock_New(
        node_cap,
        node_cap,
        size_of::<AttributeSet>() as u32,
        transmute_copy(&(AttributeSet_Free as *const c_void)),
    );
    if ptr.is_null() {
        // Handle error, e.g., return a Result instead of Self
        panic!("Failed to allocate nodes DataBlock");
    }
    ptr
};

Apply similar changes to the edges allocation as well.

Comment on lines 139 to 156
Self {
reserved_node_count: 0,
nodes: nodes,
edges: edges,
adjacency_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, true),
labels: Vec::new(),
node_labels: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
relations: Vec::new(),
zero_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
rwlock: CRWLock::new(),
writelocked: false,
matrix_policy: MatrixPolicy::FlushResize,
stats: GraphStatistics {
node_count: Vec::new(),
edge_count: Vec::new(),
},
partial: false,
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check for potential integer overflow in matrix dimensions

When initializing matrices in the Graph::new method, there's a risk of integer overflow if node_cap is very large. This could lead to unexpected behavior or panics.

Consider adding checks to ensure that node_cap doesn't exceed the maximum allowed dimension for your matrix implementations:

fn new(node_cap: u64, edge_cap: u64) -> Result<Self, &'static str> {
    if node_cap > some_max_dimension {
        return Err("Node capacity exceeds maximum allowed dimension");
    }
    
    // ... rest of the initialization code ...
    
    Ok(Self {
        // ... struct fields ...
    })
}

This change will help prevent potential issues with very large graph initializations.

Comment on lines +330 to +333
if id != INVALID_ENTITY_ID {
debug_assert!(id == n.id);
self.reserved_node_count -= 1;
debug_assert!(self.reserved_node_count >= 0);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent potential integer underflow when decrementing reserved_node_count

When decrementing reserved_node_count, there's no check to ensure it doesn't underflow. This could lead to unexpected behavior if reserved_node_count is already 0.

Add a check to prevent underflow:

if id != INVALID_ENTITY_ID {
    debug_assert!(id == n.id);
    if self.reserved_node_count > 0 {
        self.reserved_node_count -= 1;
    } else {
        // Log a warning or handle the unexpected state
    }
    debug_assert!(self.reserved_node_count >= 0);
}

This change will help prevent potential issues with unexpected decrements of reserved_node_count.

Comment on lines +346 to +349
debug_assert!(self.get_node(src).is_some());
debug_assert!(self.get_node(dest).is_some());
debug_assert!(r >= 0 && r < self.relations.len() as i32);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace debug assertions with proper validation in create_edge

The create_edge method uses debug_assert! for validating input parameters. However, these assertions are only checked in debug builds and are completely removed in release builds, potentially leading to undefined behavior if invalid inputs are provided.

Replace the debug assertions with proper validation that's active in all build configurations:

pub fn create_edge(&mut self, src: NodeID, dest: NodeID, r: RelationID, e: &mut Edge) -> Result<(), &'static str> {
    if self.get_node(src).is_none() {
        return Err("Source node does not exist");
    }
    if self.get_node(dest).is_none() {
        return Err("Destination node does not exist");
    }
    if r < 0 || r >= self.relations.len() as i32 {
        return Err("Invalid relation ID");
    }

    // ... rest of the method ...
}

This change ensures that invalid inputs are caught and handled appropriately in all build configurations.

Comment on lines 414 to 426
pub fn delete_nodes(
&mut self,
nodes: &[Node],
) {
let policy = self.set_matrix_policy(MatrixPolicy::Nop);

debug_assert!({
let mut v = Vec::new();
nodes.iter().all(|n| {
self.get_node_edges(n, GraphEdgeDir::Both, -1, &mut v);
v.is_empty()
})
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve edge checking efficiency in delete_nodes

The current implementation uses a debug assertion with a nested loop to check if nodes have no edges. This is inefficient and only works in debug builds.

Consider replacing the debug assertion with a more efficient check that works in all build configurations:

pub fn delete_nodes(&mut self, nodes: &[Node]) -> Result<(), &'static str> {
    let policy = self.set_matrix_policy(MatrixPolicy::Nop);

    for node in nodes {
        if self.get_node_degree(node, GraphEdgeDir::Both, -1) > 0 {
            return Err("Cannot delete node with existing edges");
        }
    }

    // ... rest of the method ...

    Ok(())
}

This change improves efficiency and ensures that the check is performed in all build configurations.

Comment on lines 428 to 447
let mut elems = SparseMatrix::new(
unsafe { GrB_BOOL },
self.required_matrix_dim(),
self.required_matrix_dim(),
);

for node in nodes {
let mut it = DeltaMatrixIter::new_range(&self.node_labels, node.id, node.id);

while let Ok(Some((_, j))) = it.next_bool() {
elems.set_element_bool(true, node.id, j);
self.labels[j as usize].remove_element(node.id, node.id);
self.stats.decrement_node_count(j as LabelID, 1);
}
unsafe { DataBlock_DeleteItem(self.nodes, node.id) };
}

self.node_labels.remove_elements(&elems);

self.set_matrix_policy(policy);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for matrix operations in delete_nodes

The delete_nodes method performs several matrix operations without checking for potential errors. This could lead to unexpected behavior if any of these operations fail.

Consider adding error handling for matrix operations:

pub fn delete_nodes(&mut self, nodes: &[Node]) -> Result<(), &'static str> {
    // ... existing code ...

    let mut elems = SparseMatrix::new(
        unsafe { GrB_BOOL },
        self.required_matrix_dim(),
        self.required_matrix_dim(),
    );

    for node in nodes {
        let mut it = DeltaMatrixIter::new_range(&self.node_labels, node.id, node.id);

        while let Ok(Some((_, j))) = it.next_bool() {
            elems.set_element_bool(true, node.id, j).map_err(|_| "Failed to set element in SparseMatrix")?;
            self.labels[j as usize].remove_element(node.id, node.id).map_err(|_| "Failed to remove element from label matrix")?;
            self.stats.decrement_node_count(j as LabelID, 1);
        }
        unsafe { DataBlock_DeleteItem(self.nodes, node.id) };
    }

    self.node_labels.remove_elements(&elems).map_err(|_| "Failed to remove elements from node_labels matrix")?;

    self.set_matrix_policy(policy);
    Ok(())
}

This change ensures that any errors during matrix operations are properly handled and reported.

Comment on lines +456 to +458
for e in edges.iter() {
unsafe { DataBlock_DeleteItem(self.edges, e.id) };
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider a safer approach for deleting items from the DataBlock

The current implementation uses unsafe code to delete items from the DataBlock. This could lead to undefined behavior if not used correctly.

Consider creating a safe wrapper method for DataBlock_DeleteItem that handles potential errors:

impl Graph {
    // ... other methods ...

    fn safe_delete_edge(&self, edge_id: u64) -> Result<(), &'static str> {
        let result = unsafe { DataBlock_DeleteItem(self.edges, edge_id) };
        if result != 0 {  // Assuming 0 indicates success, adjust if needed
            Err("Failed to delete edge from DataBlock")
        } else {
            Ok(())
        }
    }
}

// Then in delete_edges:
pub fn delete_edges(&mut self, edges: &mut [Edge]) -> Result<(), &'static str> {
    // ... other code ...

    for e in edges.iter() {
        self.safe_delete_edge(e.id)?;
    }

    // ... rest of the method ...
}

This approach provides a safer interface for deleting edges and allows for proper error handling.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 7131eb6 and 36fe5b7.

📒 Files selected for processing (1)
  • src/graph/ffi.rs (1 hunks)
🧰 Additional context used
🔇 Additional comments (2)
src/graph/ffi.rs (2)

1-33: LGTM: File header and imports are appropriate.

The file header includes proper copyright and license information. The imports are relevant for the FFI implementation, including necessary types and modules from both the standard library and the crate.


484-545: Improve safety in graph modification functions.

The functions Graph_EnsureNodeCap, Graph_MarkEdgeDeleted, Graph_MarkNodeDeleted, Graph_SetNode, Graph_SetNodeLabels, and Graph_SetEdge correctly wrap the corresponding methods of the Graph struct. However, they lack null pointer checks and have potential safety issues with raw pointer usage.

  1. Add null pointer checks for all input pointers.
  2. Use safer alternatives to unwrap() when dealing with potentially null pointers.
  3. Consider returning a boolean to indicate success or failure of the operation.
  4. In Graph_SetNode, use a safer method for creating a slice from raw parts.

Example improvements:

unsafe extern "C" fn Graph_EnsureNode

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +62 to +75
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetMatrixPolicy(g: *mut Graph) -> MatrixPolicy {
(&*g).get_matrix_policy()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_SetMatrixPolicy(
g: *mut Graph,
policy: MatrixPolicy,
) -> MatrixPolicy {
(&mut *g).set_matrix_policy(policy)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks for Graph_GetMatrixPolicy and Graph_SetMatrixPolicy.

While these functions correctly wrap the corresponding methods of the Graph struct, they lack null pointer checks on the graph pointer. This could lead to undefined behavior if a null pointer is passed.

Consider adding null pointer checks at the beginning of these functions:

unsafe extern "C" fn Graph_GetMatrixPolicy(g: *mut Graph) -> MatrixPolicy {
    if g.is_null() {
        return MatrixPolicy::default(); // Or some error indicator
    }
    (&*g).get_matrix_policy()
}

unsafe extern "C" fn Graph_SetMatrixPolicy(
    g: *mut Graph,
    policy: MatrixPolicy,
) -> MatrixPolicy {
    if g.is_null() {
        return MatrixPolicy::default(); // Or some error indicator
    }
    (&mut *g).set_matrix_policy(policy)
}

This change will prevent potential crashes or undefined behavior when these functions are called with a null pointer.

src/graph/ffi.rs Outdated
Comment on lines 316 to 376
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RelationshipContainsMultiEdge(
g: *mut Graph,
r: RelationID,
) -> bool {
(&mut *g).relationship_contains_multi_edge(r)
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetNode(
g: *mut Graph,
id: NodeID,
n: *mut Node,
) -> bool {
match (&*g).get_node(id) {
Some(node) => {
n.write(node);
true
}
None => false,
}
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetEdge(
g: *mut Graph,
id: EdgeID,
e: *mut Edge,
) -> bool {
(&*g).get_edge(id, e.as_mut().unwrap())
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetEdgesConnectingNodes(
g: *mut Graph,
src_id: NodeID,
dest_id: NodeID,
r: RelationID,
edges: *mut *mut Edge,
) {
let es = (&mut *g).get_edges_connecting_nodes(src_id, dest_id, r);
let mut arr_ptr = (*edges as *mut ArrayHeader).sub(1);
let mut arr = arr_ptr.as_mut().unwrap();
if arr.cap - arr.len < es.len() as u32 {
arr.cap = arr.len + es.len() as u32;
arr_ptr = RedisModule_Realloc.unwrap()(
arr_ptr as _,
(arr.cap as usize * size_of::<Edge>() + size_of::<ArrayHeader>()) as usize,
) as _;
edges.write(arr_ptr.add(1) as _);
arr = arr_ptr.as_mut().unwrap();
}
for (i, e) in es.iter().enumerate() {
(*edges).add(arr.len as usize + i).write(*e);
}
arr.len += es.len() as u32;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety and add parameter validation in graph operation functions.

The functions Graph_RelationshipContainsMultiEdge, Graph_GetNode, Graph_GetEdge, and Graph_GetEdgesConnectingNodes correctly wrap the corresponding methods of the Graph struct. However, there are potential safety issues with null pointer dereferencing and a lack of checks for some input parameters.

  1. Add null pointer checks for all input pointers.
  2. Validate input parameters where applicable.
  3. Use safer alternatives to unwrap() when dealing with potentially null pointers.
  4. Consider returning a boolean or error code to indicate success or failure of the operation.

Example improvements:

unsafe extern "C" fn Graph_RelationshipContainsMultiEdge(
    g: *mut Graph,
    r: RelationID,
) -> bool {
    if g.is_null() {
        return false;
    }
    (&mut *g).relationship_contains_multi_edge(r)
}

unsafe extern "C" fn Graph_GetNode(
    g: *mut Graph,
    id: NodeID,
    n: *mut Node,
) -> bool {
    if g.is_null() || n.is_null() {
        return false;
    }
    match (&*g).get_node(id) {
        Some(node) => {
            *n = node;
            true
        }
        None => false,
    }
}

unsafe extern "C" fn Graph_GetEdge(
    g: *mut Graph,
    id: EdgeID,
    e: *mut Edge,
) -> bool {
    if g.is_null() || e.is_null() {
        return false;
    }
    (&*g).get_edge(id, &mut *e)
}

unsafe extern "C" fn Graph_GetEdgesConnectingNodes(
    g: *mut Graph,
    src_id: NodeID,
    dest_id: NodeID,
    r: RelationID,
    edges: *mut *mut Edge,
) -> bool {
    if g.is_null() || edges.is_null() {
        return false;
    }
    // ... (rest of the function with additional safety checks)
}

These changes improve safety by checking for null pointers, validating input parameters, and providing a way for the caller to know if the operation was successful.

src/graph/ffi.rs Outdated
Comment on lines 378 to 429
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetNodeEdges(
g: *mut Graph,
n: *const Node,
dir: GraphEdgeDir,
edge_type: RelationID,
edges: *mut *mut Edge,
) {
let mut es = Vec::new();
(&mut *g).get_node_edges(n.as_ref().unwrap(), dir, edge_type, &mut es);
let mut arr_ptr = (*edges as *mut ArrayHeader).sub(1);
let mut arr = arr_ptr.as_mut().unwrap();
if arr.cap - arr.len < es.len() as u32 {
arr.cap = arr.len + es.len() as u32;
arr_ptr = RedisModule_Realloc.unwrap()(
arr_ptr as _,
(arr.cap as usize * size_of::<Edge>() + size_of::<ArrayHeader>()) as usize,
) as _;
edges.write(arr_ptr.add(1) as _);
arr = arr_ptr.as_mut().unwrap();
}
for (i, e) in es.iter().enumerate() {
(*edges).add(arr.len as usize + i).write(*e);
}
arr.len += es.len() as u32;
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetNodeDegree(
g: *mut Graph,
n: *const Node,
dir: GraphEdgeDir,
edge_type: RelationID,
) -> u64 {
(&mut *g).get_node_degree(n.as_ref().unwrap(), dir, edge_type)
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetNodeLabels(
g: *mut Graph,
n: *const Node,
labels: *mut LabelID,
label_count: u32,
) -> u32 {
(&mut *g).get_node_labels(
n.as_ref().unwrap(),
from_raw_parts_mut(labels, label_count as usize),
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enhance safety in node information retrieval functions.

The functions Graph_GetNodeEdges, Graph_GetNodeDegree, and Graph_GetNodeLabels correctly wrap the corresponding methods of the Graph struct. However, there are potential safety issues with null pointer dereferencing and the use of from_raw_parts.

  1. Add null pointer checks for all input pointers.
  2. Use safer alternatives to unwrap() when dealing with potentially null pointers.
  3. Consider returning a boolean or error code to indicate success or failure of the operation.
  4. In Graph_GetNodeEdges, consider using a safer method for array resizing.

Example improvements:

unsafe extern "C" fn Graph_GetNodeEdges(
    g: *mut Graph,
    n: *const Node,
    dir: GraphEdgeDir,
    edge_type: RelationID,
    edges: *mut *mut Edge,
) -> bool {
    if g.is_null() || n.is_null() || edges.is_null() {
        return false;
    }
    let mut es = Vec::new();
    (&mut *g).get_node_edges(n.as_ref().unwrap(), dir, edge_type, &mut es);
    // ... (implement safe array resizing logic here)
    true
}

unsafe extern "C" fn Graph_GetNodeDegree(
    g: *mut Graph,
    n: *const Node,
    dir: GraphEdgeDir,
    edge_type: RelationID,
) -> u64 {
    if g.is_null() || n.is_null() {
        return 0;
    }
    (&mut *g).get_node_degree(n.as_ref().unwrap(), dir, edge_type)
}

unsafe extern "C" fn Graph_GetNodeLabels(
    g: *mut Graph,
    n: *const Node,
    labels: *mut LabelID,
    label_count: u32,
) -> u32 {
    if g.is_null() || n.is_null() || labels.is_null() {
        return 0;
    }
    (&mut *g).get_node_labels(
        n.as_ref().unwrap(),
        std::slice::from_raw_parts_mut(labels, label_count as usize),
    )
}

These changes improve safety by checking for null pointers and provide a way for the caller to know if the operation was successful.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (4)
src/graph/graph.rs (4)

39-46: Consider adding documentation for MatrixPolicy enum

The MatrixPolicy enum is newly added, but it lacks documentation explaining the purpose and use cases for each variant. Adding documentation would improve code readability and maintainability.

Consider adding documentation comments for the MatrixPolicy enum and its variants:

/// Defines the policy for matrix operations in the graph.
#[repr(C)]
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub enum MatrixPolicy {
    /// Flush and resize the matrix.
    FlushResize,
    /// Only resize the matrix without flushing.
    Resize,
    /// No operation on the matrix.
    Nop,
}

48-54: Add documentation for GraphEdgeDir enum

The GraphEdgeDir enum is newly added but lacks documentation. Adding documentation would improve code readability and help users understand the purpose of each variant.

Consider adding documentation comments for the GraphEdgeDir enum and its variants:

/// Defines the direction of edges in the graph.
#[repr(C)]
#[derive(PartialEq, Eq)]
pub enum GraphEdgeDir {
    /// Incoming edges.
    Incoming,
    /// Outgoing edges.
    Outgoing,
    /// Both incoming and outgoing edges.
    Both,
}

1069-1240: Enhance unit tests for better coverage and error handling

While the existing unit tests provide good coverage, there are opportunities to improve their robustness and cover more edge cases.

Consider the following improvements to the unit tests:

  1. Add tests for error cases, such as creating a graph with zero capacity or allocating nodes/edges beyond capacity.
  2. Test the GraphStatistics methods to ensure they handle edge cases correctly.
  3. Add tests for the MatrixPolicy and GraphEdgeDir enums to ensure they behave as expected.
  4. Include tests for concurrent access to the graph using multiple threads.
  5. Test the Drop implementation to ensure all resources are properly freed.

Example of an additional test:

#[test]
fn test_graph_capacity_limits() {
    test_init();
    let mut g = Graph::new(5, 5);
    g.acquire_write_lock();

    // Test node capacity
    for _ in 0..5 {
        let mut n = g.reserve_node();
        g.create_node(&mut n, &[]).expect("Should create node successfully");
    }
    let mut n = g.reserve_node();
    assert!(g.create_node(&mut n, &[]).is_err(), "Should fail to create node beyond capacity");

    // Test edge capacity
    let r = g.add_relation_type();
    let mut edge = Edge::default();
    for i in 0..4 {
        g.create_edge(0, i + 1, r, &mut edge).expect("Should create edge successfully");
    }
    assert!(g.create_edge(0, 4, r, &mut edge).is_err(), "Should fail to create edge beyond capacity");

    g.release_lock();
}

These improvements will help ensure that the Graph implementation handles various scenarios correctly and remains robust under different conditions.


1-1240: Overall assessment: Robust implementation with room for improvement

The Graph implementation in src/graph/graph.rs is comprehensive and covers a wide range of functionality. However, there are several areas where the code could be improved:

  1. Error handling: Many methods could benefit from better error handling, particularly when dealing with unsafe operations and FFI calls.
  2. Safety: The use of unsafe code, while necessary for FFI, could be better documented and contained.
  3. Documentation: Adding more documentation, especially for public APIs and unsafe operations, would improve code readability and maintainability.
  4. Unit tests: While the existing tests provide good coverage, there's room for improvement in testing edge cases and error scenarios.

Consider addressing these points to make the implementation more robust, safer, and easier to maintain. Additionally, it may be worth exploring ways to reduce the use of unsafe code where possible, perhaps by wrapping unsafe operations in safe abstractions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 36fe5b7 and 97d1239.

📒 Files selected for processing (1)
  • src/graph/graph.rs (1 hunks)
🧰 Additional context used

Comment on lines +123 to +130
let nodes = unsafe {
DataBlock_New(
node_cap,
node_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety and documentation for unsafe FFI calls in Graph::new

The Graph::new method contains unsafe FFI calls to DataBlock_New without proper documentation or error handling.

Consider adding documentation and improving error handling for these unsafe calls:

pub fn new(node_cap: u64, edge_cap: u64) -> Result<Self, &'static str> {
    // Allocate nodes DataBlock
    let nodes = unsafe {
        // SAFETY: DataBlock_New is an FFI call that allocates memory.
        // It's unsafe because it can return a null pointer if allocation fails.
        let ptr = DataBlock_New(
            node_cap,
            node_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            return Err("Failed to allocate nodes DataBlock");
        }
        ptr
    };

    // Allocate edges DataBlock
    let edges = unsafe {
        // SAFETY: DataBlock_New is an FFI call that allocates memory.
        // It's unsafe because it can return a null pointer if allocation fails.
        let ptr = DataBlock_New(
            edge_cap,
            edge_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            // Free the previously allocated nodes DataBlock
            DataBlock_Free(nodes);
            return Err("Failed to allocate edges DataBlock");
        }
        ptr
    };

    // ... rest of the method ...
}

This change improves safety by handling allocation failures and adds documentation to explain why unsafe code is necessary.

Also applies to: 131-138

Comment on lines +1034 to +1067
unsafe {
let it = if self.partial {
DataBlock_FullScan(self.nodes)
} else {
self.scan_nodes()
};
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);

let it = if self.partial {
DataBlock_FullScan(self.edges)
} else {
self.scan_edges()
};
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);

DataBlock_Free(self.nodes);
DataBlock_Free(self.edges);
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety in Drop implementation for Graph

The Drop implementation for Graph contains several unsafe operations without proper documentation or error handling.

Consider adding documentation and improving error handling for these unsafe operations:

impl Drop for Graph {
    fn drop(&mut self) {
        unsafe {
            // SAFETY: DataBlock_FullScan and DataBlock_Scan are FFI calls that return an iterator.
            // We need to ensure that we free all resources properly.
            let it = if self.partial {
                DataBlock_FullScan(self.nodes)
            } else {
                self.scan_nodes()
            };
            
            // Free all node attribute sets
            loop {
                let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
                if set.is_null() {
                    break;
                }
                // SAFETY: AttributeSet_Free is an FFI call that frees memory.
                // We assume that the set pointer is valid as it comes from DataBlockIterator_Next.
                AttributeSet_Free(set);
            }
            DataBlockIterator_Free(it);

            // Similar process for edges
            // ... (omitted for brevity)

            // SAFETY: DataBlock_Free is an FFI call that frees memory.
            // We assume that self.nodes and self.edges are valid pointers.
            DataBlock_Free(self.nodes);
            DataBlock_Free(self.edges);
        }
    }
}

This change adds documentation to explain the safety considerations for each unsafe operation and improves the overall safety of the Drop implementation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 97d1239 and d9d2c1e.

📒 Files selected for processing (1)
  • src/graph/ffi.rs (1 hunks)
🧰 Additional context used

src/graph/ffi.rs Outdated
Comment on lines 527 to 531
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_SetNodeLabels(g: *mut Graph) {
(&mut *g).set_node_labels();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer check in Graph_SetNodeLabels.

Currently, g is dereferenced without checking for null, which can lead to undefined behavior if a null pointer is passed. To enhance safety, add a null pointer check for g.

Apply this diff to add the null pointer check:

 unsafe extern "C" fn Graph_SetNodeLabels(g: *mut Graph) {
+    if g.is_null() {
+        return;
+    }
     (&mut *g).set_node_labels();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_SetNodeLabels(g: *mut Graph) {
(&mut *g).set_node_labels();
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_SetNodeLabels(g: *mut Graph) {
if g.is_null() {
return;
}
(&mut *g).set_node_labels();
}

src/graph/ffi.rs Outdated
Comment on lines 547 to 552
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetDeletedNodesList(g: *mut Graph) -> *mut u64 {
(&*g).get_deleted_nodes_list()
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer check in Graph_GetDeletedNodesList.

Currently, g is dereferenced without checking for null, which can lead to undefined behavior if a null pointer is passed. To enhance safety, add a null pointer check for g.

Apply this diff to add the null pointer check:

 unsafe extern "C" fn Graph_GetDeletedNodesList(g: *mut Graph) -> *mut u64 {
+    if g.is_null() {
+        return std::ptr::null_mut();
+    }
     (&*g).get_deleted_nodes_list()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetDeletedNodesList(g: *mut Graph) -> *mut u64 {
(&*g).get_deleted_nodes_list()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetDeletedNodesList(g: *mut Graph) -> *mut u64 {
if g.is_null() {
return std::ptr::null_mut();
}
(&*g).get_deleted_nodes_list()
}

src/graph/ffi.rs Outdated
Comment on lines 553 to 558
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetDeletedEdgesList(g: *mut Graph) -> *mut u64 {
(&*g).get_deleted_edges_list()
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer check in Graph_GetDeletedEdgesList.

Currently, g is dereferenced without checking for null, which can lead to undefined behavior if a null pointer is passed. To enhance safety, add a null pointer check for g.

Apply this diff to add the null pointer check:

 unsafe extern "C" fn Graph_GetDeletedEdgesList(g: *mut Graph) -> *mut u64 {
+    if g.is_null() {
+        return std::ptr::null_mut();
+    }
     (&*g).get_deleted_edges_list()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetDeletedEdgesList(g: *mut Graph) -> *mut u64 {
(&*g).get_deleted_edges_list()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetDeletedEdgesList(g: *mut Graph) -> *mut u64 {
if g.is_null() {
return std::ptr::null_mut();
}
(&*g).get_deleted_edges_list()
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Outside diff range and nitpick comments (7)
src/graph/graph.rs (7)

45-49: Consider adding a brief comment for MatrixPolicy

The MatrixPolicy enum is introduced without any accompanying documentation. Consider adding a brief comment explaining its purpose and when each variant should be used. This will improve code readability and maintainability.


177-197: Document potential side effects of apply_all_pending

The apply_all_pending method temporarily changes the matrix policy, which could have side effects on concurrent operations. Consider adding a documentation comment to explain the purpose of this method and any potential side effects or thread-safety considerations.

For example:

/// Applies all pending changes to the graph's matrices.
///
/// This method temporarily changes the matrix policy to `FlushResize`,
/// which may affect concurrent operations. Ensure proper synchronization
/// when calling this method in a multi-threaded context.
///
/// # Arguments
///
/// * `force_flush` - If true, forces a flush of all pending changes.
pub fn apply_all_pending(&mut self, force_flush: bool) {
    // ... existing implementation ...
}

778-790: Fix typo in method name syncronize

The method name syncronize contains a typo. It should be spelled synchronize. This typo should be corrected to improve code readability and maintain consistent naming conventions.

fn synchronize(
    matrix_policy: MatrixPolicy,
    m: &mut DeltaMatrix,
    nrows: u64,
    ncols: u64,
) -> &mut DeltaMatrix {
    // ... existing implementation ...
}

Remember to update all calls to this method throughout the codebase.


1094-1105: Add comments explaining test initialization steps

The test_init function performs several initialization steps without explaining their purpose. Consider adding comments to clarify the reason for each step:

fn test_init() {
    unsafe {
        // Reset memory allocator state for clean test environment
        Alloc_Reset();
        
        // Initialize GraphBLAS in non-blocking mode
        GrB_init(GrB_Mode::GrB_NONBLOCKING);
        
        // Set GraphBLAS to use row-major format for better performance
        GxB_Global_Option_set(GxB_Option_Field::GxB_FORMAT, GxB_Format_Value::GxB_BY_ROW);
        
        // Set maximum pending changes for delta matrices
        Config_Option_set(
            ConfigOptionField::DELTA_MAX_PENDING_CHANGES,
            "10000\0".as_ptr() as *const c_char,
            null_mut(),
        );
    };
}

These comments will help other developers understand the purpose of each initialization step.


1107-1120: Enhance test coverage for test_new_graph

The current test case for creating a new graph covers basic functionality, but it could be expanded to test more properties of the newly created graph. Consider adding more assertions:

#[test]
fn test_new_graph() {
    test_init();
    let mut g = Graph::new(16384, 16384);
    g.acquire_write_lock();

    assert_eq!(g.adjacency_matrix.ncols(), g.required_matrix_dim());
    assert_eq!(g.adjacency_matrix.nrows(), g.required_matrix_dim());
    assert_eq!(g.adjacency_matrix.nvals(), 0);
    assert_eq!(g.node_count(), 0);
    
    // Add more assertions
    assert_eq!(g.edge_count(), 0);
    assert_eq!(g.label_type_count(), 0);
    assert_eq!(g.relation_type_count(), 0);
    assert!(!g.pending());

    g.release_lock();
}

These additional assertions will provide more comprehensive coverage of the initial graph state.


1122-1140: Enhance test coverage for test_graph_constructions

The current test case for graph construction covers basic node addition, but it could be expanded to test more properties after adding nodes. Consider adding more assertions:

#[test]
fn test_graph_constructions() {
    test_init();
    let node_count = 16384 / 2;
    let mut g = Graph::new(node_count, node_count);
    g.acquire_write_lock();

    for _ in 0..node_count {
        let mut n = g.reserve_node();
        g.create_node(&mut n, &[]);
    }

    assert_eq!(g.node_count(), node_count as u64);
    assert!(g.adjacency_matrix.ncols() >= node_count);
    assert!(g.adjacency_matrix.nrows() >= node_count);
    assert_eq!(g.adjacency_matrix.nvals(), 0);

    // Add more assertions
    assert_eq!(g.edge_count(), 0);
    assert_eq!(g.label_type_count(), 0);
    assert_eq!(g.relation_type_count(), 0);
    assert!(g.get_node(0).is_some());
    assert!(g.get_node(node_count as u64 - 1).is_some());
    assert!(g.get_node(node_count as u64).is_none());

    g.release_lock();
}

These additional assertions will provide more comprehensive coverage of the graph state after node addition.


1142-1183: Enhance test coverage for test_remove_nodes

The current test case for removing nodes covers basic functionality, but it could be expanded to test more properties after node and edge removal. Consider adding more assertions:

#[test]
fn test_remove_nodes() {
    test_init();
    let mut g = Graph::new(32, 32);
    g.acquire_write_lock();

    // ... existing setup code ...

    g.delete_edges(edges.as_mut_slice());
    g.delete_nodes(&[n1]);

    g.release_lock();

    assert_eq!(g.node_count(), 2);
    assert_eq!(g.edge_count(), 1);

    // Add more assertions
    assert!(g.get_node(n1.id).is_none());
    assert!(g.get_node(n2.id).is_some());
    assert!(g.get_node(n3.id).is_some());

    let mut remaining_edges = Vec::new();
    g.get_node_edges(&n2, GraphEdgeDir::Both, -1, &mut remaining_edges);
    assert_eq!(remaining_edges.len(), 1);
    assert_eq!(remaining_edges[0].src_id, n2.id);
    assert_eq!(remaining_edges[0].dest_id, n3.id);

    assert_eq!(g.get_relation_matrix(r, false).nvals(), 1);
    assert_eq!(g.get_adjacency_matrix(false).nvals(), 1);
}

These additional assertions will provide more comprehensive coverage of the graph state after node and edge removal.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between d9d2c1e and 3847403.

📒 Files selected for processing (2)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
🧰 Additional context used
🔇 Additional comments (1)
src/graph/graph.rs (1)

1185-1200: Enhance test coverage for test_get_node

The current test case for getting nodes covers basic functionality, but it could be expanded to test edge cases and invalid node retrieval. Consider adding more assertions:

Comment on lines 122 to 160
pub fn new(
node_cap: u64,
edge_cap: u64,
) -> Self {
let nodes = unsafe {
DataBlock_New(
node_cap,
node_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
let edges = unsafe {
DataBlock_New(
edge_cap,
edge_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
Self {
reserved_node_count: 0,
nodes: nodes,
edges: edges,
adjacency_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, true),
labels: Vec::new(),
node_labels: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
relations: Vec::new(),
zero_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
rwlock: CRWLock::new(),
writelocked: false,
matrix_policy: MatrixPolicy::FlushResize,
stats: GraphStatistics {
node_count: Vec::new(),
edge_count: Vec::new(),
},
partial: false,
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve error handling in Graph::new method

The Graph::new method uses unsafe calls to DataBlock_New without checking if the returned pointers are null. This could lead to undefined behavior if the allocation fails.

Consider adding null checks and error handling for the DataBlock_New calls:

pub fn new(node_cap: u64, edge_cap: u64) -> Result<Self, &'static str> {
    let nodes = unsafe {
        // SAFETY: DataBlock_New is an FFI call that allocates memory.
        // It's unsafe because it can return a null pointer if allocation fails.
        let ptr = DataBlock_New(
            node_cap,
            node_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            return Err("Failed to allocate nodes DataBlock");
        }
        ptr
    };
    
    let edges = unsafe {
        // SAFETY: DataBlock_New is an FFI call that allocates memory.
        // It's unsafe because it can return a null pointer if allocation fails.
        let ptr = DataBlock_New(
            edge_cap,
            edge_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            // Free the previously allocated nodes DataBlock
            DataBlock_Free(nodes);
            return Err("Failed to allocate edges DataBlock");
        }
        ptr
    };
    
    Ok(Self {
        // ... struct fields ...
    })
}

This change will help prevent potential issues with failed allocations and improve the overall robustness of the code.

Comment on lines +341 to +358
pub fn create_edge(
&mut self,
src: NodeID,
dest: NodeID,
r: RelationID,
e: &mut Edge,
) {
debug_assert!(self.get_node(src).is_some());
debug_assert!(self.get_node(dest).is_some());
debug_assert!(r >= 0 && r < self.relations.len() as i32);

e.src_id = src;
e.dest_id = dest;
e.relation_id = r;
e.attributes = unsafe { DataBlock_AllocateItem(self.edges, &mut e.id) } as _;

self.form_connection(src, dest, r, e);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for DataBlock_AllocateItem call in create_edge

Similar to the create_node method, create_edge uses an unsafe call to DataBlock_AllocateItem without checking if the returned pointer is null. This could lead to undefined behavior if the allocation fails.

Consider adding a null check and error handling for the DataBlock_AllocateItem call:

pub fn create_edge(&mut self, src: NodeID, dest: NodeID, r: RelationID, e: &mut Edge) -> Result<(), &'static str> {
    debug_assert!(self.get_node(src).is_some());
    debug_assert!(self.get_node(dest).is_some());
    debug_assert!(r >= 0 && r < self.relations.len() as i32);

    e.src_id = src;
    e.dest_id = dest;
    e.relation_id = r;
    e.attributes = unsafe {
        let ptr = DataBlock_AllocateItem(self.edges, &mut e.id) as *mut AttributeSet;
        if ptr.is_null() {
            return Err("Failed to allocate edge");
        }
        ptr
    };

    self.form_connection(src, dest, r, e);
    Ok(())
}

This change ensures that any errors during edge allocation are properly handled and reported.

Comment on lines +515 to +521
pub fn scan_nodes(&self) -> *mut DataBlockIterator {
unsafe { DataBlock_Scan(self.nodes) }
}

pub fn scan_edges(&self) -> *mut DataBlockIterator {
unsafe { DataBlock_Scan(self.edges) }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider adding error handling for scan operations

The scan_nodes and scan_edges methods use unsafe FFI calls without any error handling. Consider adding error checking and returning a Result type:

pub fn scan_nodes(&self) -> Result<*mut DataBlockIterator, &'static str> {
    let iterator = unsafe { DataBlock_Scan(self.nodes) };
    if iterator.is_null() {
        Err("Failed to create node iterator")
    } else {
        Ok(iterator)
    }
}

pub fn scan_edges(&self) -> Result<*mut DataBlockIterator, &'static str> {
    let iterator = unsafe { DataBlock_Scan(self.edges) };
    if iterator.is_null() {
        Err("Failed to create edge iterator")
    } else {
        Ok(iterator)
    }
}

This change will make the methods safer to use and allow callers to handle potential errors.

Comment on lines 707 to 723
pub fn get_node_labels(
&mut self,
n: &Node,
labels: &mut [i32],
) -> u32 {
let m = self.get_node_label_matrix();

let mut it = DeltaMatrixIter::new_range(m, n.id, n.id);
let mut i = 0;

while let Ok(Some((_, j))) = it.next_bool() {
labels[i as usize] = j as i32;
i += 1;
}

i
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety and flexibility in get_node_labels

The get_node_labels method has a few potential issues:

  1. There's no bounds checking when writing to the labels array, which could lead to buffer overflow.
  2. The method returns a u32, which might not be sufficient for large graphs.

Consider the following improvements:

pub fn get_node_labels(&mut self, n: &Node, labels: &mut Vec<i32>) -> usize {
    let m = self.get_node_label_matrix();
    let mut it = DeltaMatrixIter::new_range(m, n.id, n.id);
    
    labels.clear(); // Ensure the vector is empty before adding new labels
    
    while let Ok(Some((_, j))) = it.next_bool() {
        labels.push(j as i32);
    }
    
    labels.len()
}

This version:

  1. Uses a Vec<i32> instead of a fixed-size array, preventing buffer overflow.
  2. Returns a usize, which is more appropriate for counting elements.
  3. Clears the vector before adding new labels, ensuring a clean state.

Comment on lines +813 to +818
pub fn mark_edge_deleted(
&self,
id: u64,
) {
unsafe { DataBlock_MarkAsDeletedOutOfOrder(self.edges, id) };
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for DataBlock_MarkAsDeletedOutOfOrder

The mark_edge_deleted method uses an unsafe FFI call without any error handling. Consider adding error checking and returning a Result type:

pub fn mark_edge_deleted(&self, id: u64) -> Result<(), &'static str> {
    let result = unsafe { DataBlock_MarkAsDeletedOutOfOrder(self.edges, id) };
    if result != 0 { // Assuming 0 indicates success, adjust if needed
        Err("Failed to mark edge as deleted")
    } else {
        Ok(())
    }
}

Apply similar changes to the mark_node_deleted method as well. This change will make the methods safer to use and allow callers to handle potential errors.

Comment on lines +366 to +370
it: *mut EdgeIterator,
e: *mut Edge,
) -> bool {
(*it).next(e.as_mut().unwrap())
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks in EdgeIterator_Next.

In EdgeIterator_Next, it and e are dereferenced without checking for null pointers. This can lead to undefined behavior if null pointers are passed.

Apply this diff to add null pointer checks:

unsafe extern "C" fn EdgeIterator_Next(
    it: *mut EdgeIterator,
    e: *mut Edge,
) -> bool {
+   if it.is_null() || e.is_null() {
+       return false; // Handle the error appropriately
+   }
    (*it).next(e.as_mut().unwrap())
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it: *mut EdgeIterator,
e: *mut Edge,
) -> bool {
(*it).next(e.as_mut().unwrap())
}
unsafe extern "C" fn EdgeIterator_Next(
it: *mut EdgeIterator,
e: *mut Edge,
) -> bool {
if it.is_null() || e.is_null() {
return false; // Handle the error appropriately
}
(*it).next(e.as_mut().unwrap())
}

src/graph/ffi.rs Outdated
Comment on lines 354 to 361
g: *mut Graph,
it: *mut EdgeIterator,
srcID: NodeID,
destID: NodeID,
r: RelationID,
) {
(&mut *it).init(g.as_mut().unwrap(), srcID, destID, r);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks in Graph_EdgeIteratorInit.

In Graph_EdgeIteratorInit, g and it are dereferenced without checking for null pointers. This can lead to undefined behavior if null pointers are passed.

Apply this diff to add null pointer checks:

unsafe extern "C" fn Graph_EdgeIteratorInit(
    g: *mut Graph,
    it: *mut EdgeIterator,
    srcID: NodeID,
    destID: NodeID,
    r: RelationID,
) {
+   if g.is_null() || it.is_null() {
+       return; // Handle the error appropriately
+   }
    (&mut *it).init(g.as_mut().unwrap(), srcID, destID, r);
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
g: *mut Graph,
it: *mut EdgeIterator,
srcID: NodeID,
destID: NodeID,
r: RelationID,
) {
(&mut *it).init(g.as_mut().unwrap(), srcID, destID, r);
}
unsafe extern "C" fn Graph_EdgeIteratorInit(
g: *mut Graph,
it: *mut EdgeIterator,
srcID: NodeID,
destID: NodeID,
r: RelationID,
) {
if g.is_null() || it.is_null() {
return; // Handle the error appropriately
}
(&mut *it).init(g.as_mut().unwrap(), srcID, destID, r);
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 29

🧹 Outside diff range and nitpick comments (1)
src/graph/graph.rs (1)

42-57: Consider reviewing the usage of MatrixPolicy variants.

The MatrixPolicy enum is marked with #[allow(dead_code)], which suggests that some variants might not be used. It's worth reviewing if all variants are necessary or if some can be removed to improve code clarity.

The GraphEdgeDir enum looks well-defined for representing edge directions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 3847403 and 72dfaf6.

📒 Files selected for processing (5)
  • src/binding/cmutex.rs (1 hunks)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
  • src/graph/matrix/delta_matrix.rs (7 hunks)
  • src/graph/matrix/tensor.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/binding/cmutex.rs
🧰 Additional context used
🔇 Additional comments (9)
src/graph/ffi.rs (1)

511-523: Enhance safety in Graph_SetEdge function.

The Graph_SetEdge function lacks null pointer checks and uses unwrap() on a potentially null pointer. Consider the following improvements:

  1. Add null pointer checks for the graph and edge pointers.
  2. Use a safer alternative to unwrap() when dealing with the potentially null edge pointer.
  3. Return
src/graph/graph.rs (2)

1-40: LGTM: Imports and constant definitions look good.

The imports cover both standard library and custom modules, which seem appropriate for the graph implementation. The copyright notice is present, and the INVALID_ENTITY_ID constant is defined using a common practice.


1-1273: Overall assessment: Solid implementation with room for improvement

The src/graph/graph.rs file implements a complex graph data structure with various operations. The implementation is generally solid and includes unit tests, which is commendable. However, there are several areas where the code can be improved:

  1. Error handling: Many methods, especially those involving unsafe operations, could benefit from more robust error handling and reporting.
  2. Safety: Some unsafe operations lack proper documentation explaining their safety considerations. Adding such documentation would improve code maintainability and safety.
  3. Bounds checking: Some methods in GraphStatistics and elsewhere could benefit from additional bounds checking to prevent potential panics or undefined behavior.
  4. Memory management: The Drop implementation for Graph could be improved with better documentation and potentially more robust error handling during resource cleanup.

Addressing these points would significantly enhance the overall quality, safety, and maintainability of the code.

src/graph/matrix/tensor.rs (1)

46-46: Confirm necessity of unsafe usage in Tensor::new.

In line 46, the use of unsafe { GrB_UINT64 } might not be necessary if GrB_UINT64 is already safe to use. Consider avoiding unsafe if possible or minimizing its scope.

src/graph/matrix/delta_matrix.rs (5)

8-11: Proper use of cmutex::CMutex for mutex management

The import of cmutex::CMutex replaces the custom mutex implementation, enhancing code reuse and maintainability by utilizing a centralized mutex module.


79-81: Consider returning Option<&DeltaMatrix> instead of Option<&Box<DeltaMatrix>>

As previously noted, returning Option<&DeltaMatrix> simplifies the interface by avoiding unnecessary indirection and prevents exposing the Box to the caller.


83-84: Consider returning Option<&mut DeltaMatrix> instead of Option<&mut Box<DeltaMatrix>>

This issue remains relevant from prior reviews. Refactoring to return Option<&mut DeltaMatrix> would eliminate unnecessary indirection and improve the method's ergonomics.


103-106: Addition of m_mut provides necessary mutable access

The new method m_mut correctly offers mutable access to self.matrix, facilitating in-place modifications when required.


559-559: Variable a shadows previous declaration in test_new_matrix

As previously mentioned, the variable a is redeclared, which can lead to confusion and potential bugs. Consider renaming the second a to avoid shadowing.

Comment on lines +39 to +67
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ApplyAllPending(
g: *mut Graph,
force_flush: bool,
) {
(&mut *g).apply_all_pending(force_flush);
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetMatrixPolicy(g: *mut Graph) -> MatrixPolicy {
(&*g).get_matrix_policy()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_SetMatrixPolicy(
g: *mut Graph,
policy: MatrixPolicy,
) -> MatrixPolicy {
(&mut *g).set_matrix_policy(policy)
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_Pending(g: *mut Graph) -> bool {
(&*g).pending()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Enhance safety and error handling in graph operation functions.

The functions Graph_ApplyAllPending, Graph_GetMatrixPolicy, Graph_SetMatrixPolicy, and Graph_Pending lack null pointer checks and some lack error handling. Consider the following improvements:

  1. Add null pointer checks for the graph pointer in all functions.
  2. Implement error handling and return a boolean to indicate success/failure for Graph_ApplyAllPending.

Example implementation for Graph_ApplyAllPending:

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ApplyAllPending(
    g: *mut Graph,
    force_flush: bool,
) -> bool {
    if g.is_null() {
        return false;
    }
    match (&mut *g).apply_all_pending(force_flush) {
        Ok(_) => true,
        Err(_) => false,
    }
}

Apply similar null pointer checks to the other functions in this group.

Comment on lines +78 to +114
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AddLabel(g: *mut Graph) -> LabelID {
(&mut *g).add_label()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelNode(
g: *mut Graph,
id: NodeID,
lbls: *mut LabelID,
lbl_count: u32,
) {
(&mut *g).label_node(id, from_raw_parts(lbls, lbl_count as usize));
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RemoveNodeLabels(
g: *mut Graph,
id: NodeID,
lbls: *mut LabelID,
lbl_count: u32,
) {
(&mut *g).remove_node_labels(id, from_raw_parts(lbls, lbl_count as usize));
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_IsNodeLabeled(
g: *mut Graph,
id: NodeID,
l: LabelID,
) -> bool {
(&mut *g).is_node_labeled(id, l)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Improve safety in label-related functions.

The label-related functions (Graph_AddLabel, Graph_LabelNode, Graph_RemoveNodeLabels, and Graph_IsNodeLabeled) have potential safety issues. Consider the following improvements:

  1. Add null pointer checks for the graph pointer in all functions.
  2. In Graph_LabelNode and Graph_RemoveNodeLabels, add checks for null lbls pointer when lbl_count is non-zero.
  3. Use a safer alternative to from_raw_parts.

Example improvement for Graph_LabelNode:

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelNode(
    g: *mut Graph,
    id: NodeID,
    lbls: *mut LabelID,
    lbl_count: u32,
) -> bool {
    if g.is_null() {
        return false;
    }
    let labels = if lbl_count == 0 {
        &[]
    } else {
        if lbls.is_null() {
            return false;
        }
        std::slice::from_raw_parts(lbls, lbl_count as usize)
    };
    (&mut *g).label_node(id, labels);
    true
}

Apply similar changes to other functions in this group.

Comment on lines +116 to +150
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AddRelationType(g: *mut Graph) -> RelationID {
(&mut *g).add_relation_type()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AllocateNodes(
g: *mut Graph,
n: usize,
) {
(&*g).allocate_nodes(n);
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AllocateEdges(
g: *mut Graph,
n: usize,
) {
(&*g).allocate_edges(n);
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ResetReservedNode(g: *mut Graph) {
(&mut *g).reset_reserved_node();
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ReserveNode(g: *mut Graph) -> Node {
(&mut *g).reserve_node()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Enhance safety and error handling in graph operation functions.

The functions Graph_AddRelationType, Graph_AllocateNodes, Graph_AllocateEdges, Graph_ResetReservedNode, and Graph_ReserveNode lack null pointer checks and some don't provide ways to handle potential errors. Consider the following improvements:

  1. Add null pointer checks for the graph pointer in all functions.
  2. Return boolean values to indicate success or failure, especially for operations that might fail (e.g., allocation).
  3. For Graph_ReserveNode, consider how to handle potential failures in node reservation.

Example improvements:

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AllocateNodes(
    g: *mut Graph,
    n: usize,
) -> bool {
    if g.is_null() {
        return false;
    }
    match (&*g).allocate_nodes(n) {
        Ok(_) => true,
        Err(_) => false,
    }
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ReserveNode(g: *mut Graph) -> Option<Node> {
    if g.is_null() {
        return None;
    }
    (&mut *g).reserve_node().ok()
}

Apply similar changes to other functions in this group.

Comment on lines +194 to +212
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeleteNodes(
g: *mut Graph,
nodes: *mut Node,
count: u64,
) {
(&mut *g).delete_nodes(from_raw_parts(nodes, count as usize));
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeleteEdges(
g: *mut Graph,
edges: *mut Edge,
count: u64,
) {
(&mut *g).delete_edges(from_raw_parts_mut(edges, count as usize));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Enhance safety in node and edge deletion functions.

The functions Graph_DeleteNodes and Graph_DeleteEdges have potential safety issues. Consider the following improvements:

  1. Add null pointer checks for the graph and nodes/edges pointers.
  2. Use safer alternatives to from_raw_parts when dealing with potentially null pointers.
  3. Return a boolean to indicate success or failure of the operation.

Example improvements:

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeleteNodes(
    g: *mut Graph,
    nodes: *mut Node,
    count: u64,
) -> bool {
    if g.is_null() || nodes.is_null() {
        return false;
    }
    let nodes_slice = std::slice::from_raw_parts(nodes, count as usize);
    (&mut *g).delete_nodes(nodes_slice);
    true
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeleteEdges(
    g: *mut Graph,
    edges: *mut Edge,
    count: u64,
) -> bool {
    if g.is_null() || edges.is_null() {
        return false;
    }
    let edges_slice = std::slice::from_raw_parts_mut(edges, count as usize);
    (&mut *g).delete_edges(edges_slice);
    true
}

These changes improve safety by checking for null pointers and provide a way for the caller to know if the operation was successful.

Comment on lines +214 to +222
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_EntityIsDeleted(ge: *mut GraphEntity) -> bool {
if (&*ge).attributes.is_null() {
return false;
}

DataBlock_ItemIsDeleted((&*ge).attributes as _)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Add null pointer check in Graph_EntityIsDeleted.

The Graph_EntityIsDeleted function checks if the attributes pointer is null, but it doesn't check if the input pointer ge is null. Consider adding a null pointer check for ge:

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_EntityIsDeleted(ge: *mut GraphEntity) -> bool {
    if ge.is_null() {
        return false; // Or consider returning a special value to indicate error
    }
    if (&*ge).attributes.is_null() {
        return false;
    }
    DataBlock_ItemIsDeleted((&*ge).attributes as _)
}

This change will prevent potential crashes or undefined behavior when the function is called with a null pointer.

Comment on lines +411 to +414
unsafe {
GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle errors from iterator functions in TensorRangeIterator::next.

In the next method of TensorRangeIterator, the return values of GxB_Vector_Iterator_attach and GxB_Vector_Iterator_seek are ignored. Ensure that you check and handle these return values to prevent undefined behavior.

Apply this diff:

                     unsafe {
                         let info_attach = GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
+                        if info_attach != GrB_Info::GrB_SUCCESS {
+                            // Handle the error appropriately
+                            return None;
+                        }
                         let info_seek = GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0);
+                        if info_seek != GrB_Info::GrB_SUCCESS {
+                            // Handle the error appropriately
+                            return None;
+                        }
                     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
unsafe {
GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
};
unsafe {
let info_attach = GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
if info_attach != GrB_Info::GrB_SUCCESS {
// Handle the error appropriately
return None;
}
let info_seek = GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0);
if info_seek != GrB_Info::GrB_SUCCESS {
// Handle the error appropriately
return None;
}
};

Comment on lines +197 to +200
GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
let mut it = it.assume_init();
GxB_Vector_Iterator_seek(&mut it, 0);
let index = GxB_Vector_Iterator_getIndex(&mut it);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check for null pointer before using GxB_Vector_Iterator_getIndex.

In remove_elements, after attaching the iterator, you should verify that the iterator is valid before using it. This prevents potential null pointer dereferences.

Apply this diff to add a null pointer check:

                     let mut it = it.assume_init();
                     GxB_Vector_Iterator_seek(&mut it, 0);
+                    if it.is_null() {
+                        // Handle the error appropriately
+                        return;
+                    }
                     let index = GxB_Vector_Iterator_getIndex(&mut it);

Committable suggestion was skipped due to low confidence.

Comment on lines +370 to +373
GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
};
self.eit = Some(unsafe { it.assume_init() });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check return values of GraphBLAS iterator functions in TensorIterator::next.

In the next method of TensorIterator, the return values of GxB_Vector_Iterator_attach and GxB_Vector_Iterator_seek are not checked. It's important to handle potential errors from these functions.

Apply this diff to handle errors:

                 unsafe {
                     let info_attach = GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
+                    if info_attach != GrB_Info::GrB_SUCCESS {
+                        // Handle the error appropriately
+                        // For example, set self.deleted = true and return None
+                        self.deleted = true;
+                        return None;
+                    }
                     let info_seek = GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0);
+                    if info_seek != GrB_Info::GrB_SUCCESS {
+                        // Handle the error appropriately
+                        self.deleted = true;
+                        return None;
+                    }
                 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
};
self.eit = Some(unsafe { it.assume_init() });
unsafe {
let info_attach = GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
if info_attach != GrB_Info::GrB_SUCCESS {
// Handle the error appropriately
// For example, set self.deleted = true and return None
self.deleted = true;
return None;
}
let info_seek = GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0);
if info_seek != GrB_Info::GrB_SUCCESS {
// Handle the error appropriately
self.deleted = true;
return None;
}
};
self.eit = Some(unsafe { it.assume_init() });

Comment on lines +133 to +146

if to - from == 1 {
self.m
.set_element_u64(edges[from].as_ref().unwrap().id, src, dest);
} else {
let mut v = MaybeUninit::uninit();
GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
let v = v.assume_init();
self.m.set_element_u64(set_msb(v as u64), src, dest);
for j in from..to {
GrB_Vector_setElement_BOOL(v, true, edges[j].as_ref().unwrap().id);
}
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Free allocated GrB_Vector in set_elements to prevent memory leaks.

In the set_elements method, after using the GrB_Vector v, it should be freed to prevent memory leaks.

Apply this diff:

                     GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
+                    GrB_Vector_free(&mut v);
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if to - from == 1 {
self.m
.set_element_u64(edges[from].as_ref().unwrap().id, src, dest);
} else {
let mut v = MaybeUninit::uninit();
GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
let v = v.assume_init();
self.m.set_element_u64(set_msb(v as u64), src, dest);
for j in from..to {
GrB_Vector_setElement_BOOL(v, true, edges[j].as_ref().unwrap().id);
}
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
}
if to - from == 1 {
self.m
.set_element_u64(edges[from].as_ref().unwrap().id, src, dest);
} else {
let mut v = MaybeUninit::uninit();
GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
let v = v.assume_init();
self.m.set_element_u64(set_msb(v as u64), src, dest);
for j in from..to {
GrB_Vector_setElement_BOOL(v, true, edges[j].as_ref().unwrap().id);
}
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
GrB_Vector_free(&mut v);
}

@@ -522,7 +496,7 @@ impl DeltaMatrix {
return;
}

self.mutex.as_mut().unwrap().lock();
self.mutex.as_ref().unwrap().lock();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unwrapping self.mutex without checking may cause panics

In the synchronize method, calling self.mutex.as_ref().unwrap() assumes that self.mutex is always Some, which could lead to a panic if it's None. Ensure that self.mutex is always initialized or consider handling the Option more safely.

Apply this diff to handle potential None values:

-        self.mutex.as_ref().unwrap().lock();
+        if let Some(mutex) = &self.mutex {
+            mutex.lock();
+        } else {
+            // Handle the None case appropriately
+        }

Also applies to: 509-509

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 22

🧹 Outside diff range and nitpick comments (12)
src/graph/matrix/tensor.rs (2)

24-34: Add documentation for bit manipulation utilities.

These utility functions handle critical edge ID operations but lack documentation explaining their purpose and usage.

Add rustdoc comments explaining:

+/// Returns true if the edge ID represents a single edge (MSB is not set).
 pub fn single_edge(current_edge: EdgeID) -> bool {
     current_edge as u64 & (1u64 << (u64::BITS as usize - 1)) == 0
 }

+/// Sets the Most Significant Bit (MSB) of the edge ID to mark it as a multi-edge.
 pub fn set_msb(meid: EdgeID) -> u64 {
     meid as u64 | (1u64 << (u64::BITS as usize - 1))
 }

+/// Clears the Most Significant Bit (MSB) of the edge ID to access the underlying value.
 pub fn clear_msb(meid: EdgeID) -> u64 {
     meid as u64 & !(1u64 << (u64::BITS as usize - 1))
 }

36-38: Add architectural documentation for the Tensor implementation.

The Tensor struct implements a complex graph data structure using bit manipulation and GraphBLAS, but lacks documentation explaining:

  • The overall design and data representation
  • How single vs. multiple edges are handled
  • The relationship between Tensor and DeltaMatrix
  • Memory management strategies

Add module-level documentation:

+/// # Tensor
+/// 
+/// A graph tensor implementation that efficiently handles both single and multiple edges
+/// between vertices using a combination of bit manipulation and GraphBLAS operations.
+/// 
+/// ## Design
+/// - Single edges are stored directly in the matrix
+/// - Multiple edges use the MSB to indicate a vector of edge IDs
+/// - The underlying DeltaMatrix provides efficient sparse matrix operations
+/// 
+/// ## Memory Management
+/// GraphBLAS vectors are allocated for multiple edges and must be freed when:
+/// - Removing edges
+/// - Converting from single to multiple edges
+/// - Finishing iteration over multiple edges
 pub struct Tensor {
     pub m: DeltaMatrix,
 }
src/graph/graph.rs (2)

1-38: Consider adding module-level documentation.

The file lacks module-level documentation explaining its purpose, key types, and usage patterns. Consider adding a doc comment at the module level to help users understand the graph implementation's capabilities and design choices.

Add documentation like this at the start of the file:

//! Graph implementation providing a concurrent, matrix-based representation of nodes and edges.
//! 
//! This module implements a graph data structure using GraphBLAS matrices for efficient
//! storage and operations. It supports:
//! - Concurrent access through read-write locks
//! - Dynamic node and edge management
//! - Label and relationship type management
//! - Matrix-based graph operations

1058-1242: Enhance test coverage for error cases.

The tests primarily cover successful operations but should also verify error handling.

Consider adding tests for:

  • Allocation failures
  • Invalid node/edge IDs
  • Concurrent access scenarios
  • Resource exhaustion
  • Error propagation

Example test:

#[test]
fn test_create_node_error_handling() {
    test_init();
    let mut g = Graph::new(1, 1).unwrap(); // Minimal capacity
    g.acquire_write_lock();
    
    // First node should succeed
    let mut n1 = g.reserve_node();
    assert!(g.create_node(&mut n1, &[]).is_ok());
    
    // Second node should fail due to capacity
    let mut n2 = g.reserve_node();
    assert!(matches!(g.create_node(&mut n2, &[]), Err(_)));
    
    g.release_lock();
}
src/binding/crwlock.rs (1)

50-54: Check for errors in the Drop implementation.

In the Drop trait implementation, the call to pthread_rwlock_destroy does not handle potential errors. While failures are rare, it's good practice to check the return value to ensure the lock is destroyed properly.

Consider updating the drop method:

     fn drop(&mut self) {
-        unsafe { libc::pthread_rwlock_destroy(self.rwlock.get()) };
+        unsafe {
+            let res = libc::pthread_rwlock_destroy(self.rwlock.get());
+            debug_assert!(res == 0, "pthread_rwlock_destroy failed");
+        }
     }
src/graph/matrix/delta_matrix.rs (4)

22-25: Suggestion: Make DeltaMatrixBase Struct Private

The DeltaMatrixBase struct is currently declared as public, but it seems to be used only within this module. Making it private can encapsulate the implementation details and reduce the public API surface.

Apply this diff to make DeltaMatrixBase private:

-pub struct DeltaMatrixBase {
+struct DeltaMatrixBase {
     pub matrix: SparseMatrix,
     pub delta_plus: SparseMatrix,
     pub delta_minus: SparseMatrix,
 }

35-37: Clarify the Purpose of the dirty Flag

The dirty flag is introduced to track pending changes, but it's not immediately clear how and when it's set or cleared. Adding documentation or comments would help other developers understand its role in the synchronization process.

Consider adding a comment like:

/// Indicates whether there are pending changes that require synchronization.
dirty: bool,

Line range hint 279-305: Ensure Proper Resource Management in remove_elements Method

In the remove_elements method, GrB_Scalar_new and GrB_Scalar_free are used. Ensure that all allocated resources are properly freed, even in case of errors, to prevent memory leaks.

Review the code to confirm that GrB_Scalar_free is called in all execution paths.


Line range hint 573-583: Prevent Deadlocks by Ensuring Mutex is Always Released

In the synchronize method, self.mutex.lock() is called, but there is a return statement before self.mutex.unlock(), which can lead to a deadlock if the condition is met.

Apply this diff to use a scoped lock guard:

pub fn synchronize(
    &mut self,
    nrows: u64,
    ncols: u64,
) {
    if !(self.nrows() < nrows || self.ncols() < ncols || self.dirty) {
        return;
    }

    self.mutex.lock();
    // Ensure that unlock is called even if early returns happen
    defer! {
        self.mutex.unlock();
    }

    // Rest of the method...
}

Alternatively, consider using RAII to automatically release the lock.

src/graph/matrix/ffi.rs (3)

Line range hint 17-22: Add null pointer check for output parameter a in Delta_Matrix_new

In the function Delta_Matrix_new, the output parameter a is dereferenced without checking if it is null. This can lead to undefined behavior if a is null.

Apply this diff to add a null pointer check:

 unsafe extern "C" fn Delta_Matrix_new(
     a: *mut *mut DeltaMatrix,
     ty: GrB_Type,
     nrows: GrB_Index,
     ncols: GrB_Index,
     transpose: bool,
 ) -> GrB_Info {
+    if a.is_null() {
+        return GrB_Info::GrB_NULL_POINTER;
+    }
     *a = Box::into_raw(Box::new(DeltaMatrix::new(ty, nrows, ncols, transpose)));
     GrB_Info::GrB_SUCCESS
 }

Line range hint 66-70: Add null pointer check for input parameter c in Delta_Matrix_setElement_BOOL

In Delta_Matrix_setElement_BOOL, the parameter c is dereferenced without checking if it is null.

Apply this diff:

 unsafe extern "C" fn Delta_Matrix_setElement_BOOL(
     c: *mut DeltaMatrix,
     i: GrB_Index,
     j: GrB_Index,
 ) -> GrB_Info {
+    if c.is_null() {
+        return GrB_Info::GrB_NULL_POINTER;
+    }
     (*c).set_element_bool(i, j);
     GrB_Info::GrB_SUCCESS
 }

Line range hint 75-83: Add null pointer checks in Delta_Matrix_extractElement_BOOL

Ensure that x and c are not null before dereferencing them.

Apply this diff:

 unsafe extern "C" fn Delta_Matrix_extractElement_BOOL(
     x: *mut bool,
     c: *mut DeltaMatrix,
     i: GrB_Index,
     j: GrB_Index,
 ) -> GrB_Info {
+    if x.is_null() || c.is_null() {
+        return GrB_Info::GrB_NULL_POINTER;
+    }
     if let Some(v) = (&*c).extract_element_bool(i, j) {
         *x = v;
         GrB_Info::GrB_SUCCESS
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 72dfaf6 and f62f630.

📒 Files selected for processing (7)
  • src/binding/crwlock.rs (1 hunks)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
  • src/graph/matrix/delta_matrix.rs (33 hunks)
  • src/graph/matrix/delta_matrix_iter.rs (4 hunks)
  • src/graph/matrix/ffi.rs (5 hunks)
  • src/graph/matrix/tensor.rs (1 hunks)
🔇 Additional comments (33)
src/graph/graph.rs (3)

122-160: ⚠️ Potential issue

Improve error handling in Graph::new.

The constructor uses unsafe FFI calls without proper error handling, which could lead to undefined behavior if allocations fail.

Modify the constructor to handle allocation failures:

pub fn new(node_cap: u64, edge_cap: u64) -> Result<Self, &'static str> {
    let nodes = unsafe {
        let ptr = DataBlock_New(
            node_cap,
            node_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            return Err("Failed to allocate nodes DataBlock");
        }
        ptr
    };
    
    let edges = unsafe {
        let ptr = DataBlock_New(
            edge_cap,
            edge_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            // Clean up previously allocated resources
            DataBlock_Free(nodes);
            return Err("Failed to allocate edges DataBlock");
        }
        ptr
    };
    
    Ok(Self { /* ... */ })
}

Likely invalid or redundant comment.


59-103: ⚠️ Potential issue

Add bounds checking to GraphStatistics methods.

The methods in GraphStatistics directly index into vectors without bounds checking, which could lead to panics if invalid indices are provided.

Consider adding bounds checking and returning Results:

impl GraphStatistics {
-    fn increment_node_count(&mut self, l: LabelID, arg: u64) {
-        self.node_count[l as usize] += arg;
+    fn increment_node_count(&mut self, l: LabelID, arg: u64) -> Result<(), &'static str> {
+        self.node_count.get_mut(l as usize)
+            .map(|count| *count += arg)
+            .ok_or("Invalid label ID")
     }

-    fn decrement_node_count(&mut self, l: LabelID, arg: u64) {
-        self.node_count[l as usize] -= arg;
+    fn decrement_node_count(&mut self, l: LabelID, arg: u64) -> Result<(), &'static str> {
+        self.node_count.get_mut(l as usize)
+            .map(|count| *count = count.saturating_sub(arg))
+            .ok_or("Invalid label ID")
     }
}

Likely invalid or redundant comment.


894-929: ⚠️ Potential issue

Improve safety in Drop implementation.

The Drop implementation contains unsafe code without proper error handling or documentation.

Add safety documentation and improve error handling:

impl Drop for Graph {
    fn drop(&mut self) {
        unsafe {
            // SAFETY: DataBlock_FullScan and DataBlock_Scan are FFI calls that return an iterator.
            // We need to ensure that we free all resources properly.
            let it = if self.partial {
                DataBlock_FullScan(self.nodes)
            } else {
                self.scan_nodes()
            };
+            if it.is_null() {
+                // Log error but continue cleanup
+                eprintln!("Warning: Failed to create node iterator during cleanup");
+            }
            
            // Free all node attribute sets
            loop {
                let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
                if set.is_null() {
                    break;
                }
                AttributeSet_Free(set);
            }
            DataBlockIterator_Free(it);

            // Similar error handling for edges cleanup
            // ...
        }
    }
}

Likely invalid or redundant comment.

src/binding/crwlock.rs (2)

17-26: Add error handling for pthread_rwlock_init in new().

The previous review comment about handling errors in new() is still applicable. Currently, debug_assert! is used, which only checks in debug builds. It's recommended to propagate initialization errors to the caller.


35-40: Add error handling for pthread_rwlock_wrlock in acquire_write().

The previous review comment regarding error handling in acquire_write() remains valid. Please consider propagating errors to the caller instead of using debug_assert!.

src/graph/matrix/delta_matrix_iter.rs (5)

20-25: Addition of 'transpose' parameter in new_range enhances flexibility

The inclusion of the transpose: bool parameter in the new_range constructor allows the DeltaMatrixIter to handle transposed matrices, increasing its versatility.


28-32: Correct utilization of transpose in iterator initialization

Using the transpose parameter when initializing m_it, dp_it, and dm_it ensures that the iterators correctly reflect the transposition state of the matrices.


42-42: Inclusion of transpose parameter in attach_range method

Adding the transpose: bool parameter to attach_range provides consistent control over the transposition state during reattachment, aligning with the changes made in new_range.


47-49: Proper initialization of iterators with transpose in attach_range

Updating m_it, dp_it, and dm_it with the transpose parameter ensures that the iterators are correctly configured when attach_range is called.


156-156: Test cases updated with transpose parameter

The test functions now pass false for the transpose parameter when creating a new DeltaMatrixIter, ensuring that the tests are aligned with the updated constructor.

Also applies to: 177-177

src/graph/ffi.rs (11)

23-25: Null pointer checks are missing in lock functions

The previous review comments about adding null pointer checks and error handling in Graph_AcquireReadLock, Graph_AcquireWriteLock, and Graph_ReleaseLock are still valid and applicable to these code segments.

Also applies to: 29-31, 35-37


50-52: Add null pointer checks in matrix policy functions

The earlier review comments regarding adding null pointer checks to Graph_GetMatrixPolicy and Graph_SetMatrixPolicy remain relevant for these functions.

Also applies to: 57-61


86-93: Ensure safe handling of label pointers in node label functions

The prior comments about potential null pointer dereferences in Graph_LabelNode and Graph_RemoveNodeLabels are still applicable. The issues concerning the handling of lbls when lbl_count is greater than zero need to be addressed.

Also applies to: 98-104


161-167: Null pointer checks needed for node and edge creation functions

Previous review comments about adding null pointer checks in Graph_CreateNode and Graph_CreateEdge are still valid. The use of as_mut().unwrap() without null checks can lead to panics if null pointers are passed.

Also applies to: 179-180


216-222: Add null pointer check in Graph_EntityIsDeleted

The earlier comment about the potential null pointer dereference in Graph_EntityIsDeleted remains applicable. Adding a null check for ge is necessary to enhance safety.


314-325: Null pointer checks required in Graph_GetNode and Graph_GetEdge

Previous comments regarding the need for null pointer checks in Graph_GetNode and Graph_GetEdge are still relevant. These functions should handle null pointers appropriately to prevent undefined behavior.

Also applies to: 330-335


352-356: Add null pointer checks in edge iterator functions

The past review comments about potential null pointer dereferences in EdgeIterator_Next and NodeEdgeIterator_Next are still valid. Ensuring that pointers are not null before dereferencing is critical.

Also applies to: 373-382


384-393: Ensure safety in node information retrieval functions

The previous concerns about null pointer handling in Graph_GetNodeDegree and Graph_GetNodeLabels remain applicable. Adding necessary null checks will enhance the safety of these functions.

Also applies to: 397-407


445-450: Potential issues in Graph_PartialFree and Graph_Free

Earlier comments about the risk of memory leaks or double-free errors in Graph_PartialFree and Graph_Free are still pertinent. Differentiating the two functions or consolidating them appropriately is recommended.

Also applies to: 452-456


479-483: Incorrect parameter type in Graph_MarkNodeDeleted

The previous review pointing out that the id parameter should likely be a NodeID instead of an EdgeID in Graph_MarkNodeDeleted remains valid.


536-547: Null pointer checks needed in tensor iterator functions

The past comments about adding null checks in TensorIterator_ScanRange, TensorIterator_next, and TensorIterator_is_attached are still applicable. Ensuring that pointers are validated before use is crucial for safety.

Also applies to: 552-571, 576-580

src/graph/matrix/delta_matrix.rs (11)

36-37: Potential Concurrency Issue with CMutex Usage

The mutex field is now a direct instance of CMutex instead of an Option. Ensure that the lock is properly acquired and released in all methods that modify shared data to prevent deadlocks or race conditions.

Review all methods that mutate shared state to confirm that they correctly use self.mutex.lock() and self.mutex.unlock(). Ensure that locks are not held longer than necessary.


216-234: Consistent Handling in set_element_u64 and set_element_bool

The logic in set_element_u64 differs from set_element_bool in how it updates matrix and delta_plus. Ensure that both methods handle additions and deletions consistently.

Review the logic in both methods to confirm they align, especially regarding updates to the transpose matrices.


247-252: Optimize Element Extraction Logic

The extract_element_bool method checks delta_plus, then delta_minus, then matrix. This can be inefficient if these matrices are large. Consider optimizing by using a more efficient data structure or caching mechanism.

[performance]

One option is to maintain a single combined matrix or to use a hash map for quick lookups.


846-847: Verify Correctness of test_fuzzy Results

In the test_fuzzy test, you compare a.matrix.matrix with m and a.transpose.unwrap().matrix with t. Ensure that all pending operations are flushed before comparison to get accurate results.

Call a.wait(true); before comparison to ensure all pending changes are applied.

Also, ensure that matrix_eq correctly compares the matrices, considering the potential presence of pending operations or differences in internal representations.


Line range hint 923-938: Clarify Logic in test_mxm

In the test_mxm test, c.mxm is called with b that has pending changes, whereas d.mxm is called after b.wait(true);. The test assumes that both results should be equal, but pending changes in b could lead to different outputs.

Ensure that the test accounts for pending changes. If mxm does not automatically flush pending operations, consider calling b.wait(true); before both mxm calls or clarify the expected behavior in comments.


998-1012: Ensure Correct Resizing in test_resize

In the test_resize test, after resizing the matrices, verify that all internal matrices (matrix, delta_plus, delta_minus) have been resized correctly, including in the transpose.

Add assertions to check the sizes of all internal matrices after resizing.

Example:

assert_eq!(a.matrix.matrix.nrows(), nrows);
assert_eq!(a.matrix.delta_plus.nrows(), nrows);
// Repeat for ncols and transpose matrices

860-866: Ensure Consistency in test_export_no_changes

In the test_export_no_changes test, after exporting the matrix, confirm that n is identical to a.matrix.matrix, especially considering any pending operations.

If there are pending changes, ensure they are applied before comparison by calling a.wait(true);.


882-885: Validate Export Functionality with Pending Changes

In test_export_pending_changes, you export the matrix while there are pending changes. Verify that the exported matrix includes these changes as expected.

Consider adding assertions to compare n with the expected result, accounting for pending additions and deletions.


142-143: ⚠️ Potential issue

Verify Correctness of nvals Calculation

The nvals method computes the number of values as:

self.matrix.matrix.nvals() + self.matrix.delta_plus.nvals() - self.matrix.delta_minus.nvals()

This calculation assumes that there are no overlapping elements between delta_plus and delta_minus. If an element is both added and deleted, this might result in incorrect counts.

Create test cases where an element is added and then deleted (or vice versa) to ensure that nvals returns the correct value in all scenarios.


192-203: ⚠️ Potential issue

Ensure Thread Safety in set_element_bool Method

The set_element_bool method modifies shared data without explicit synchronization. If DeltaMatrix is used in a concurrent context, this could lead to race conditions.

Confirm that access to shared mutable data is properly synchronized using the mutex. If necessary, wrap mutations in mutex locks:

self.mutex.lock();
// Mutation code
self.mutex.unlock();

Alternatively, consider using Rust's concurrency primitives like std::sync::Mutex for safer thread handling.


661-671: ⚠️ Potential issue

Correct the Assertions in test_simple_set

In the test_simple_set test, after calling a.wait(false);, the assertions check that a.matrix.matrix.nvals() is zero. However, depending on the behavior of wait(false), this may not be correct.

Review the purpose of wait(false) in this context and adjust the assertions accordingly. If wait(false) does not force synchronization, a.matrix.matrix.nvals() might still be zero.

Consider adjusting to:

assert_eq!(a.matrix.delta_plus.nvals(), 1);

If synchronization is expected, use wait(true) instead.

src/graph/matrix/ffi.rs (1)

157-160: Avoid using read_unaligned() and write_unaligned() when alignment is guaranteed

The previous review comment regarding the use of read_unaligned() and write_unaligned() in Delta_Matrix_free is still applicable. Since pointers from Rust allocations are guaranteed to be aligned, it's safer and more idiomatic to use read() and write() instead.

Here is the suggested change:

 unsafe extern "C" fn Delta_Matrix_free(c: *mut *mut DeltaMatrix) {
-    drop(Box::from_raw(c.read_unaligned()));
-    c.write_unaligned(null_mut());
+    drop(Box::from_raw(c.read()));
+    c.write(null_mut());
 }

Comment on lines +241 to +260
pub fn row_degree(
&self,
id: u64,
) -> u64 {
let mut degree = 0;
let mut it = DeltaMatrixIter::new_range(&self.m, id, id, false);
while let Ok(Some((_, _, id))) = it.next_u64() {
if single_edge(id) {
degree += 1;
} else {
let v = clear_msb(id) as GrB_Vector;
let mut nvals = MaybeUninit::uninit();
unsafe {
GrB_Vector_nvals(nvals.as_mut_ptr(), v);
degree += nvals.assume_init();
}
}
}
degree
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for GraphBLAS operations in row_degree.

The method uses unsafe GraphBLAS operations without checking their return status.

Add error handling:

     pub fn row_degree(&self, id: u64) -> u64 {
-        let mut degree = 0;
+        let mut degree = 0u64;
         let mut it = DeltaMatrixIter::new_range(&self.m, id, id, false);
         while let Ok(Some((_, _, id))) = it.next_u64() {
             if single_edge(id) {
                 degree += 1;
             } else {
                 let v = clear_msb(id) as GrB_Vector;
                 let mut nvals = MaybeUninit::uninit();
                 unsafe {
-                    GrB_Vector_nvals(nvals.as_mut_ptr(), v);
-                    degree += nvals.assume_init();
+                    match GrB_Vector_nvals(nvals.as_mut_ptr(), v) {
+                        GrB_Info::GrB_SUCCESS => degree += nvals.assume_init(),
+                        err => {
+                            // Handle error appropriately
+                            log::error!("Failed to get vector values: {:?}", err);
+                            return 0;
+                        }
+                    }
                 }
             }
         }
         degree
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn row_degree(
&self,
id: u64,
) -> u64 {
let mut degree = 0;
let mut it = DeltaMatrixIter::new_range(&self.m, id, id, false);
while let Ok(Some((_, _, id))) = it.next_u64() {
if single_edge(id) {
degree += 1;
} else {
let v = clear_msb(id) as GrB_Vector;
let mut nvals = MaybeUninit::uninit();
unsafe {
GrB_Vector_nvals(nvals.as_mut_ptr(), v);
degree += nvals.assume_init();
}
}
}
degree
}
pub fn row_degree(
&self,
id: u64,
) -> u64 {
let mut degree = 0u64;
let mut it = DeltaMatrixIter::new_range(&self.m, id, id, false);
while let Ok(Some((_, _, id))) = it.next_u64() {
if single_edge(id) {
degree += 1;
} else {
let v = clear_msb(id) as GrB_Vector;
let mut nvals = MaybeUninit::uninit();
unsafe {
match GrB_Vector_nvals(nvals.as_mut_ptr(), v) {
GrB_Info::GrB_SUCCESS => degree += nvals.assume_init(),
err => {
// Handle error appropriately
log::error!("Failed to get vector values: {:?}", err);
return 0;
}
}
}
}
}
degree
}

Comment on lines +56 to +75
if let Some(current_edge) = self.m.extract_element_u64(src, dest) {
unsafe {
if single_edge(current_edge) {
let mut v = MaybeUninit::uninit();
GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
let v = v.assume_init();
self.m.set_element_u64(set_msb(v as u64), src, dest);
GrB_Vector_setElement_BOOL(v, true, current_edge);
GrB_Vector_setElement_BOOL(v, true, id);
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
} else {
let v = clear_msb(current_edge) as GrB_Vector;
GrB_Vector_setElement_BOOL(v, true, id);
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
}
}
} else {
self.m.set_element_u64(id, src, dest);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix memory leak in set_element.

The GrB_Vector created in the if single_edge branch is never freed.

Add vector cleanup:

                     GrB_Vector_setElement_BOOL(v, true, current_edge);
                     GrB_Vector_setElement_BOOL(v, true, id);
                     GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
+                    GrB_Vector_free(&mut v);
                 } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(current_edge) = self.m.extract_element_u64(src, dest) {
unsafe {
if single_edge(current_edge) {
let mut v = MaybeUninit::uninit();
GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
let v = v.assume_init();
self.m.set_element_u64(set_msb(v as u64), src, dest);
GrB_Vector_setElement_BOOL(v, true, current_edge);
GrB_Vector_setElement_BOOL(v, true, id);
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
} else {
let v = clear_msb(current_edge) as GrB_Vector;
GrB_Vector_setElement_BOOL(v, true, id);
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
}
}
} else {
self.m.set_element_u64(id, src, dest);
}
}
if let Some(current_edge) = self.m.extract_element_u64(src, dest) {
unsafe {
if single_edge(current_edge) {
let mut v = MaybeUninit::uninit();
GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
let v = v.assume_init();
self.m.set_element_u64(set_msb(v as u64), src, dest);
GrB_Vector_setElement_BOOL(v, true, current_edge);
GrB_Vector_setElement_BOOL(v, true, id);
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
GrB_Vector_free(&mut v);
} else {
let v = clear_msb(current_edge) as GrB_Vector;
GrB_Vector_setElement_BOOL(v, true, id);
GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
}
}
} else {
self.m.set_element_u64(id, src, dest);
}
}

Comment on lines +355 to +386
impl Iterator for TensorIterator {
type Item = (NodeID, NodeID, EdgeID);

fn next(&mut self) -> Option<Self::Item> {
if self.deleted {
return None;
}
if single_edge(self.edge_id) {
self.deleted = true;
Some((self.src_id, self.dest_id, self.edge_id))
} else {
if self.eit.is_none() {
let v = clear_msb(self.edge_id) as GrB_Vector;
let mut it = MaybeUninit::uninit();
unsafe {
GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
};
self.eit = Some(unsafe { it.assume_init() });
}
unsafe {
let edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
if info == GrB_Info::GxB_EXHAUSTED {
self.eit = None;
self.deleted = true;
}
return Some((self.src_id, self.dest_id, edge_id));
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety in TensorIterator::next.

The iterator implementation has several safety concerns:

  1. No null pointer checks after iterator attachment
  2. Unchecked GraphBLAS operations
  3. Potential use-after-free when accessing the vector

Add safety checks:

     fn next(&mut self) -> Option<Self::Item> {
         if self.deleted {
             return None;
         }
         if single_edge(self.edge_id) {
             self.deleted = true;
             Some((self.src_id, self.dest_id, self.edge_id))
         } else {
             if self.eit.is_none() {
                 let v = clear_msb(self.edge_id) as GrB_Vector;
                 let mut it = MaybeUninit::uninit();
                 unsafe {
-                    GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
-                    GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
+                    if GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut()) != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
+                    if GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0) != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
                 };
                 self.eit = Some(unsafe { it.assume_init() });
             }
             unsafe {
+                let Some(it) = self.eit.as_mut() else {
+                    return None;
+                };
-                let edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
-                let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
+                let edge_id = GxB_Vector_Iterator_getIndex(it);
+                let info = GxB_Vector_Iterator_next(it);
                 if info == GrB_Info::GxB_EXHAUSTED {
                     self.eit = None;
                     self.deleted = true;
                 }
                 return Some((self.src_id, self.dest_id, edge_id));
             }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
impl Iterator for TensorIterator {
type Item = (NodeID, NodeID, EdgeID);
fn next(&mut self) -> Option<Self::Item> {
if self.deleted {
return None;
}
if single_edge(self.edge_id) {
self.deleted = true;
Some((self.src_id, self.dest_id, self.edge_id))
} else {
if self.eit.is_none() {
let v = clear_msb(self.edge_id) as GrB_Vector;
let mut it = MaybeUninit::uninit();
unsafe {
GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
};
self.eit = Some(unsafe { it.assume_init() });
}
unsafe {
let edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
if info == GrB_Info::GxB_EXHAUSTED {
self.eit = None;
self.deleted = true;
}
return Some((self.src_id, self.dest_id, edge_id));
}
}
}
}
impl Iterator for TensorIterator {
type Item = (NodeID, NodeID, EdgeID);
fn next(&mut self) -> Option<Self::Item> {
if self.deleted {
return None;
}
if single_edge(self.edge_id) {
self.deleted = true;
Some((self.src_id, self.dest_id, self.edge_id))
} else {
if self.eit.is_none() {
let v = clear_msb(self.edge_id) as GrB_Vector;
let mut it = MaybeUninit::uninit();
unsafe {
if GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut()) != GrB_Info::GrB_SUCCESS {
self.deleted = true;
return None;
}
if GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0) != GrB_Info::GrB_SUCCESS {
self.deleted = true;
return None;
}
};
self.eit = Some(unsafe { it.assume_init() });
}
unsafe {
let Some(it) = self.eit.as_mut() else {
return None;
};
let edge_id = GxB_Vector_Iterator_getIndex(it);
let info = GxB_Vector_Iterator_next(it);
if info == GrB_Info::GxB_EXHAUSTED {
self.eit = None;
self.deleted = true;
}
return Some((self.src_id, self.dest_id, edge_id));
}
}
}
}

Comment on lines 42 to 47
pub fn release(&self) {
unsafe {
let res = libc::pthread_rwlock_unlock(self.rwlock.get());
debug_assert!(res == 0, "pthread_rwlock_unlock failed");
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle errors in release().

Similar to the other methods, release() uses debug_assert! to check the result of pthread_rwlock_unlock, which won't catch errors in release builds. To ensure robustness, modify the method to return a Result<(), std::io::Error> and handle any unlocking errors appropriately.

Here's how you can adjust the method:

-    pub fn release(&self) {
+    pub fn release(&self) -> Result<(), std::io::Error> {
         unsafe {
             let res = libc::pthread_rwlock_unlock(self.rwlock.get());
-            debug_assert!(res == 0, "pthread_rwlock_unlock failed");
+            if res != 0 {
+                Err(std::io::Error::from_raw_os_error(res))
+            } else {
+                Ok(())
+            }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn release(&self) {
unsafe {
let res = libc::pthread_rwlock_unlock(self.rwlock.get());
debug_assert!(res == 0, "pthread_rwlock_unlock failed");
}
}
pub fn release(&self) -> Result<(), std::io::Error> {
unsafe {
let res = libc::pthread_rwlock_unlock(self.rwlock.get());
if res != 0 {
Err(std::io::Error::from_raw_os_error(res))
} else {
Ok(())
}
}
}

Comment on lines 28 to 33
pub fn acquire_read(&self) {
unsafe {
let res = libc::pthread_rwlock_rdlock(self.rwlock.get());
debug_assert!(res == 0, "pthread_rwlock_rdlock failed");
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Propagate errors in acquire_read().

Currently, acquire_read() uses debug_assert! to check the result of pthread_rwlock_rdlock, which only operates in debug builds. In release builds, failures in acquiring the read lock would go unnoticed. Consider modifying the method to return a Result<(), std::io::Error> and propagate errors to the caller.

Apply the following changes to handle errors properly:

-    pub fn acquire_read(&self) {
+    pub fn acquire_read(&self) -> Result<(), std::io::Error> {
         unsafe {
             let res = libc::pthread_rwlock_rdlock(self.rwlock.get());
-            debug_assert!(res == 0, "pthread_rwlock_rdlock failed");
+            if res != 0 {
+                Err(std::io::Error::from_raw_os_error(res))
+            } else {
+                Ok(())
+            }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn acquire_read(&self) {
unsafe {
let res = libc::pthread_rwlock_rdlock(self.rwlock.get());
debug_assert!(res == 0, "pthread_rwlock_rdlock failed");
}
}
pub fn acquire_read(&self) -> Result<(), std::io::Error> {
unsafe {
let res = libc::pthread_rwlock_rdlock(self.rwlock.get());
if res != 0 {
Err(std::io::Error::from_raw_os_error(res))
} else {
Ok(())
}
}
}

Comment on lines +57 to 61
c: *mut DeltaMatrix,
) -> GrB_Info {
*nvals = (&*c).nvals();
GrB_Info::GrB_SUCCESS
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks in Delta_Matrix_nvals

In Delta_Matrix_nvals, check for null pointers before dereferencing nvals and c.

Apply this diff:

 unsafe extern "C" fn Delta_Matrix_nvals(
     nvals: *mut GrB_Index,
     c: *mut DeltaMatrix,
 ) -> GrB_Info {
+    if nvals.is_null() || c.is_null() {
+        return GrB_Info::GrB_NULL_POINTER;
+    }
     *nvals = (&*c).nvals();
     GrB_Info::GrB_SUCCESS
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c: *mut DeltaMatrix,
) -> GrB_Info {
*nvals = (&*c).nvals();
GrB_Info::GrB_SUCCESS
}
unsafe extern "C" fn Delta_Matrix_nvals(
nvals: *mut GrB_Index,
c: *mut DeltaMatrix,
) -> GrB_Info {
if nvals.is_null() || c.is_null() {
return GrB_Info::GrB_NULL_POINTER;
}
*nvals = (&*c).nvals();
GrB_Info::GrB_SUCCESS
}

Comment on lines +37 to 41
c: *mut DeltaMatrix,
) -> GrB_Info {
*nrows = (&*c).nrows();
GrB_Info::GrB_SUCCESS
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks in Delta_Matrix_nrows

In Delta_Matrix_nrows, the input parameters nrows and c are dereferenced without null checks. This can lead to undefined behavior if either is null.

Apply this diff:

 unsafe extern "C" fn Delta_Matrix_nrows(
     nrows: *mut GrB_Index,
     c: *mut DeltaMatrix,
 ) -> GrB_Info {
+    if nrows.is_null() || c.is_null() {
+        return GrB_Info::GrB_NULL_POINTER;
+    }
     *nrows = (&*c).nrows();
     GrB_Info::GrB_SUCCESS
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c: *mut DeltaMatrix,
) -> GrB_Info {
*nrows = (&*c).nrows();
GrB_Info::GrB_SUCCESS
}
unsafe extern "C" fn Delta_Matrix_nrows(
nrows: *mut GrB_Index,
c: *mut DeltaMatrix,
) -> GrB_Info {
if nrows.is_null() || c.is_null() {
return GrB_Info::GrB_NULL_POINTER;
}
*nrows = (&*c).nrows();
GrB_Info::GrB_SUCCESS
}

Comment on lines +108 to 117
c: *mut DeltaMatrix,
semiring: GrB_Semiring,
a: _Matrix,
b: _Matrix,
a: *mut DeltaMatrix,
b: *mut DeltaMatrix,
desc: GrB_Descriptor,
) -> GrB_Info {
(*c).element_wise_add(semiring, &*a, &*b);
(*c).element_wise_add(semiring, &*a, &*b, desc);
GrB_Info::GrB_SUCCESS
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks in Delta_eWiseAdd

In Delta_eWiseAdd, check that c, a, and b are not null before use.

Apply this diff:

 unsafe extern "C" fn Delta_eWiseAdd(
     c: *mut DeltaMatrix,
     semiring: GrB_Semiring,
     a: *mut DeltaMatrix,
     b: *mut DeltaMatrix,
     desc: GrB_Descriptor,
 ) -> GrB_Info {
+    if c.is_null() || a.is_null() || b.is_null() {
+        return GrB_Info::GrB_NULL_POINTER;
+    }
     (*c).element_wise_add(semiring, &*a, &*b, desc);
     GrB_Info::GrB_SUCCESS
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c: *mut DeltaMatrix,
semiring: GrB_Semiring,
a: _Matrix,
b: _Matrix,
a: *mut DeltaMatrix,
b: *mut DeltaMatrix,
desc: GrB_Descriptor,
) -> GrB_Info {
(*c).element_wise_add(semiring, &*a, &*b);
(*c).element_wise_add(semiring, &*a, &*b, desc);
GrB_Info::GrB_SUCCESS
}
unsafe extern "C" fn Delta_eWiseAdd(
c: *mut DeltaMatrix,
semiring: GrB_Semiring,
a: *mut DeltaMatrix,
b: *mut DeltaMatrix,
desc: GrB_Descriptor,
) -> GrB_Info {
if c.is_null() || a.is_null() || b.is_null() {
return GrB_Info::GrB_NULL_POINTER;
}
(*c).element_wise_add(semiring, &*a, &*b, desc);
GrB_Info::GrB_SUCCESS
}

Comment on lines +29 to 31
unsafe extern "C" fn Delta_Matrix_M(c: *mut DeltaMatrix) -> GrB_Matrix {
(&*c).m(false).grb_matrix_ref()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer check for input parameter c in Delta_Matrix_M

In the function Delta_Matrix_M, the input parameter c is dereferenced without checking if it is null, which could lead to undefined behavior.

Apply this diff to add a null pointer check:

 unsafe extern "C" fn Delta_Matrix_M(c: *mut DeltaMatrix) -> GrB_Matrix {
+    if c.is_null() {
+        return null_mut();
+    }
     (&*c).m(false).grb_matrix_ref()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
unsafe extern "C" fn Delta_Matrix_M(c: *mut DeltaMatrix) -> GrB_Matrix {
(&*c).m(false).grb_matrix_ref()
}
unsafe extern "C" fn Delta_Matrix_M(c: *mut DeltaMatrix) -> GrB_Matrix {
if c.is_null() {
return null_mut();
}
(&*c).m(false).grb_matrix_ref()
}

Comment on lines +93 to 102
#[allow(non_snake_case)]
unsafe extern "C" fn Delta_mxm(
c: _Matrix,
c: *mut DeltaMatrix,
semiring: GrB_Semiring,
a: _Matrix,
b: _Matrix,
a: *mut DeltaMatrix,
b: *mut DeltaMatrix,
desc: GrB_Descriptor,
) -> GrB_Info {
(*c).mxm(semiring, &*a, &*b);
(*c).mxm(semiring, &*a, &*b, desc);
GrB_Info::GrB_SUCCESS
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks in Delta_mxm

In Delta_mxm, input parameters c, a, and b are dereferenced without null checks.

Apply this diff:

 unsafe extern "C" fn Delta_mxm(
     c: *mut DeltaMatrix,
     semiring: GrB_Semiring,
     a: *mut DeltaMatrix,
     b: *mut DeltaMatrix,
     desc: GrB_Descriptor,
 ) -> GrB_Info {
+    if c.is_null() || a.is_null() || b.is_null() {
+        return GrB_Info::GrB_NULL_POINTER;
+    }
     (*c).mxm(semiring, &*a, &*b, desc);
     GrB_Info::GrB_SUCCESS
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[allow(non_snake_case)]
unsafe extern "C" fn Delta_mxm(
c: _Matrix,
c: *mut DeltaMatrix,
semiring: GrB_Semiring,
a: _Matrix,
b: _Matrix,
a: *mut DeltaMatrix,
b: *mut DeltaMatrix,
desc: GrB_Descriptor,
) -> GrB_Info {
(*c).mxm(semiring, &*a, &*b);
(*c).mxm(semiring, &*a, &*b, desc);
GrB_Info::GrB_SUCCESS
#[allow(non_snake_case)]
unsafe extern "C" fn Delta_mxm(
c: *mut DeltaMatrix,
semiring: GrB_Semiring,
a: *mut DeltaMatrix,
b: *mut DeltaMatrix,
desc: GrB_Descriptor,
) -> GrB_Info {
if c.is_null() || a.is_null() || b.is_null() {
return GrB_Info::GrB_NULL_POINTER;
}
(*c).mxm(semiring, &*a, &*b, desc);
GrB_Info::GrB_SUCCESS

barakb and others added 3 commits November 19, 2024 09:44
This give some performance advantages as well as ergonomic advantage as rust code can use the underline parking_lot object with more safe and idiomatic API.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Outside diff range and nitpick comments (6)
src/graph/matrix/tensor.rs (3)

24-34: Add documentation for bit manipulation utilities.

These utility functions are crucial for edge ID manipulation but lack documentation explaining their purpose and usage. Consider adding doc comments to explain:

  • The significance of the MSB in edge identification
  • When and why these functions should be used
  • The relationship between single edges and multi-edges

40-48: Consider using const for GrB_UINT64.

The unsafe block in new() could be replaced with a const declaration at module level:

+const GRB_UINT64_TYPE: GrB_Type = unsafe { GrB_UINT64 };

impl Tensor {
    pub fn new(nrows: u64, ncols: u64) -> Self {
        Self {
-           m: DeltaMatrix::new(unsafe { GrB_UINT64 }, nrows, ncols, true),
+           m: DeltaMatrix::new(GRB_UINT64_TYPE, nrows, ncols, true),
        }
    }
}

312-353: Consider refactoring degree calculations to reduce code duplication.

The row_degree and col_degree methods share similar logic. Consider extracting the common functionality into a private helper method:

+    fn calculate_degree(&self, id: u64, transposed: bool) -> u64 {
+        let mut degree = 0;
+        let mut it = DeltaMatrixIter::new_range(&self.m, id, id, transposed);
+        
+        while let Ok(Some((src, dest))) = if transposed { 
+            it.next_bool().map(|r| r.map(|(d, s)| (s, d))) 
+        } else { 
+            it.next_u64().map(|r| r.map(|(s, d, _)| (s, d)))
+        } {
+            let id = self.m.extract_element_u64(src, dest).unwrap();
+            if single_edge(id) {
+                degree += 1;
+            } else {
+                let v = clear_msb(id) as GrB_Vector;
+                let mut nvals = MaybeUninit::uninit();
+                unsafe {
+                    GrB_Vector_nvals(nvals.as_mut_ptr(), v);
+                    degree += nvals.assume_init();
+                }
+            }
+        }
+        degree
+    }
+
+    pub fn row_degree(&self, id: u64) -> u64 {
+        self.calculate_degree(id, false)
+    }
+
+    pub fn col_degree(&self, id: u64) -> u64 {
+        self.calculate_degree(id, true)
+    }
src/graph/graph.rs (2)

12-24: Add documentation for unsafe FFI imports

The code imports several unsafe FFI functions without documenting their safety requirements and invariants. Consider adding module-level documentation explaining the safety requirements for these FFI functions.


1039-1223: Improve test coverage for error conditions

The current tests cover basic functionality but lack tests for:

  • Error conditions in matrix operations
  • Edge cases with invalid inputs
  • Concurrent access scenarios
  • Memory allocation failures

Consider adding tests for these scenarios to ensure robust error handling.

src/binding/graph.rs (1)

62-68: Consider refactoring to reduce code duplication in set_attributes methods

Both Node and Edge structs have identical set_attributes methods. Since you have introduced the GraphEntity struct, consider refactoring to have Node and Edge leverage GraphEntity to share this common functionality. This adheres to the DRY (Don't Repeat Yourself) principle and improves maintainability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f62f630 and defa836.

📒 Files selected for processing (4)
  • src/binding/graph.rs (4 hunks)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
  • src/graph/matrix/tensor.rs (1 hunks)
🔇 Additional comments (2)
src/binding/graph.rs (2)

10-12: Verify impact of changing ID types to unsigned integers

The types NodeID, EdgeID, and EntityID have been changed from i64 to u64. Changing from signed to unsigned integers may introduce issues if negative values are used or if there is existing code that relies on signed integer behavior. Please ensure that all usages of these types in the codebase are compatible with the new unsigned types to prevent potential integer overflows or logic errors.


218-218: Ensure proper memory management for returned pointer

The function DataBlock_DeletedItems returns a raw pointer to u64. It's important to manage the lifetime and ownership of this pointer correctly to prevent memory leaks or undefined behavior. Please verify that the calling code appropriately handles the allocation and deallocation of this memory.

Comment on lines +39 to +46
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ApplyAllPending(
g: *mut Graph,
force_flush: bool,
) {
(&mut *g).apply_all_pending(force_flush);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for pending operations.

The Graph_ApplyAllPending function doesn't handle potential errors from the underlying operation, which could lead to silent failures.

Consider returning a boolean to indicate success/failure:

unsafe extern "C" fn Graph_ApplyAllPending(
    g: *mut Graph,
    force_flush: bool,
-) {
+) -> bool {
+    if g.is_null() {
+        return false;
+    }
-    (&mut *g).apply_all_pending(force_flush);
+    match (&mut *g).apply_all_pending(force_flush) {
+        Ok(_) => true,
+        Err(_) => false,
+    }
}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +122 to +138
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AllocateNodes(
g: *mut Graph,
n: usize,
) {
(&*g).allocate_nodes(n);
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AllocateEdges(
g: *mut Graph,
n: usize,
) {
(&*g).allocate_edges(n);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add allocation failure handling.

The allocation functions don't handle potential allocation failures, which could lead to crashes.

Add error handling:

unsafe extern "C" fn Graph_AllocateNodes(
    g: *mut Graph,
    n: usize,
-) {
+) -> bool {
+    if g.is_null() {
+        return false;
+    }
-    (&*g).allocate_nodes(n);
+    match (&*g).allocate_nodes(n) {
+        Ok(_) => true,
+        Err(_) => false,
+    }
}

unsafe extern "C" fn Graph_AllocateEdges(
    g: *mut Graph,
    n: usize,
-) {
+) -> bool {
+    if g.is_null() {
+        return false;
+    }
-    (&*g).allocate_edges(n);
+    match (&*g).allocate_edges(n) {
+        Ok(_) => true,
+        Err(_) => false,
+    }
}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +409 to +443
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetAdjacencyMatrix(g: *mut Graph) -> *mut DeltaMatrix {
(&mut *g).get_adjacency_matrix()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetLabelMatrix(
g: *mut Graph,
label: LabelID,
) -> *mut DeltaMatrix {
(&mut *g).get_label_matrix(label)
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetRelationMatrix(
g: *mut Graph,
relation_idx: RelationID,
) -> *mut DeltaMatrix {
(&mut *g).get_relation_matrix(relation_idx)
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetNodeLabelMatrix(g: *mut Graph) -> *mut DeltaMatrix {
(&mut *g).get_node_label_matrix()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_GetZeroMatrix(g: *mut Graph) -> *mut DeltaMatrix {
(&mut *g).get_zero_matrix()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for matrix retrieval functions.

The matrix retrieval functions don't handle potential null returns from the underlying operations.

Consider adding error handling:

unsafe extern "C" fn Graph_GetAdjacencyMatrix(
    g: *mut Graph,
-) -> *mut DeltaMatrix {
+) -> Option<*mut DeltaMatrix> {
+    if g.is_null() {
+        return None;
+    }
-    (&mut *g).get_adjacency_matrix()
+    Some((&mut *g).get_adjacency_matrix())
}

Apply similar changes to other matrix retrieval functions.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +84 to +93
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelNode(
g: *mut Graph,
id: NodeID,
lbls: *mut LabelID,
lbl_count: u32,
) {
(&mut *g).label_node(id, from_raw_parts(lbls, lbl_count as usize));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add integer overflow protection for label count.

Converting lbl_count to usize without checking for overflow could lead to memory safety issues.

Add overflow checks:

unsafe extern "C" fn Graph_LabelNode(
    g: *mut Graph,
    id: NodeID,
    lbls: *mut LabelID,
    lbl_count: u32,
) {
+    let count = usize::try_from(lbl_count).expect("Label count overflow");
+    if lbl_count > 0 {
+        assert!(!lbls.is_null(), "Label pointer is null");
+    }
-    (&mut *g).label_node(id, from_raw_parts(lbls, lbl_count as usize));
+    (&mut *g).label_node(id, from_raw_parts(lbls, count));
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelNode(
g: *mut Graph,
id: NodeID,
lbls: *mut LabelID,
lbl_count: u32,
) {
(&mut *g).label_node(id, from_raw_parts(lbls, lbl_count as usize));
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelNode(
g: *mut Graph,
id: NodeID,
lbls: *mut LabelID,
lbl_count: u32,
) {
let count = usize::try_from(lbl_count).expect("Label count overflow");
if lbl_count > 0 {
assert!(!lbls.is_null(), "Label pointer is null");
}
(&mut *g).label_node(id, from_raw_parts(lbls, count));
}

src/graph/ffi.rs Outdated
Comment on lines 480 to 498
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_OptimizedFormConnections(
g: *mut Graph,
r: RelationID,
srcs: *const NodeID,
dests: *const NodeID,
ids: *const EdgeID,
n: u64,
multi_edge: bool,
) {
(&mut *g).optimized_form_connections(
r,
from_raw_parts(srcs, n as usize),
from_raw_parts(dests, n as usize),
from_raw_parts(ids, n as usize),
multi_edge,
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add array length validation for optimized connections.

The function uses multiple arrays that must have the same length, but there's no validation to ensure this.

Add length validation:

unsafe extern "C" fn Graph_OptimizedFormConnections(
    g: *mut Graph,
    r: RelationID,
    srcs: *const NodeID,
    dests: *const NodeID,
    ids: *const EdgeID,
    n: u64,
    multi_edge: bool,
) {
+    if g.is_null() || srcs.is_null() || dests.is_null() || ids.is_null() {
+        return;
+    }
+    let count = usize::try_from(n).expect("Length overflow");
    (&mut *g).optimized_form_connections(
        r,
-        from_raw_parts(srcs, n as usize),
-        from_raw_parts(dests, n as usize),
-        from_raw_parts(ids, n as usize),
+        from_raw_parts(srcs, count),
+        from_raw_parts(dests, count),
+        from_raw_parts(ids, count),
        multi_edge,
    );
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_OptimizedFormConnections(
g: *mut Graph,
r: RelationID,
srcs: *const NodeID,
dests: *const NodeID,
ids: *const EdgeID,
n: u64,
multi_edge: bool,
) {
(&mut *g).optimized_form_connections(
r,
from_raw_parts(srcs, n as usize),
from_raw_parts(dests, n as usize),
from_raw_parts(ids, n as usize),
multi_edge,
);
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_OptimizedFormConnections(
g: *mut Graph,
r: RelationID,
srcs: *const NodeID,
dests: *const NodeID,
ids: *const EdgeID,
n: u64,
multi_edge: bool,
) {
if g.is_null() || srcs.is_null() || dests.is_null() || ids.is_null() {
return;
}
let count = usize::try_from(n).expect("Length overflow");
(&mut *g).optimized_form_connections(
r,
from_raw_parts(srcs, count),
from_raw_parts(dests, count),
from_raw_parts(ids, count),
multi_edge,
);
}

Comment on lines +242 to +300
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_NodeCount(g: *mut Graph) -> u64 {
(&*g).node_count()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeletedNodeCount(g: *mut Graph) -> u64 {
(&*g).deleted_node_count()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_UncompactedNodeCount(g: *mut Graph) -> u64 {
(&*g).uncompacted_node_count()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabeledNodeCount(
g: *mut Graph,
label: i32,
) -> u64 {
(&*g).labeled_node_count(label)
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_EdgeCount(g: *mut Graph) -> u64 {
(&*g).edge_count()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RelationEdgeCount(
g: *mut Graph,
relation_idx: i32,
) -> u64 {
(&*g).relation_edge_count(relation_idx)
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeletedEdgeCount(g: *mut Graph) -> u32 {
(&*g).deleted_edge_count()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RelationTypeCount(g: *mut Graph) -> i32 {
(&*g).relation_type_count()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelTypeCount(g: *mut Graph) -> i32 {
(&*g).label_type_count()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Standardize return types for count functions.

The count functions use inconsistent return types (u64, u32, i32), which could lead to confusion and potential overflow issues.

Consider standardizing return types:

-unsafe extern "C" fn Graph_DeletedEdgeCount(g: *mut Graph) -> u32 {
+unsafe extern "C" fn Graph_DeletedEdgeCount(g: *mut Graph) -> u64 {
-    (&*g).deleted_edge_count()
+    (&*g).deleted_edge_count() as u64
}

-unsafe extern "C" fn Graph_RelationTypeCount(g: *mut Graph) -> i32 {
+unsafe extern "C" fn Graph_RelationTypeCount(g: *mut Graph) -> u64 {
-    (&*g).relation_type_count()
+    (&*g).relation_type_count() as u64
}

-unsafe extern "C" fn Graph_LabelTypeCount(g: *mut Graph) -> i32 {
+unsafe extern "C" fn Graph_LabelTypeCount(g: *mut Graph) -> u64 {
-    (&*g).label_type_count()
+    (&*g).label_type_count() as u64
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_NodeCount(g: *mut Graph) -> u64 {
(&*g).node_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeletedNodeCount(g: *mut Graph) -> u64 {
(&*g).deleted_node_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_UncompactedNodeCount(g: *mut Graph) -> u64 {
(&*g).uncompacted_node_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabeledNodeCount(
g: *mut Graph,
label: i32,
) -> u64 {
(&*g).labeled_node_count(label)
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_EdgeCount(g: *mut Graph) -> u64 {
(&*g).edge_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RelationEdgeCount(
g: *mut Graph,
relation_idx: i32,
) -> u64 {
(&*g).relation_edge_count(relation_idx)
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeletedEdgeCount(g: *mut Graph) -> u32 {
(&*g).deleted_edge_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RelationTypeCount(g: *mut Graph) -> i32 {
(&*g).relation_type_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelTypeCount(g: *mut Graph) -> i32 {
(&*g).label_type_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_NodeCount(g: *mut Graph) -> u64 {
(&*g).node_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeletedNodeCount(g: *mut Graph) -> u64 {
(&*g).deleted_node_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_UncompactedNodeCount(g: *mut Graph) -> u64 {
(&*g).uncompacted_node_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabeledNodeCount(
g: *mut Graph,
label: i32,
) -> u64 {
(&*g).labeled_node_count(label)
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_EdgeCount(g: *mut Graph) -> u64 {
(&*g).edge_count()
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RelationEdgeCount(
g: *mut Graph,
relation_idx: i32,
) -> u64 {
(&*g).relation_edge_count(relation_idx)
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_DeletedEdgeCount(g: *mut Graph) -> u64 {
(&*g).deleted_edge_count() as u64
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_RelationTypeCount(g: *mut Graph) -> u64 {
(&*g).relation_type_count() as u64
}
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_LabelTypeCount(g: *mut Graph) -> u64 {
(&*g).label_type_count() as u64
}

unsafe {
self.edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
debug_assert!(info == GrB_Info::GrB_SUCCESS);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace debug assertions with proper error handling.

Debug assertions are used to check GraphBLAS operation results. These should be replaced with proper error handling as they will be stripped in release builds:

-                        debug_assert!(info == GrB_Info::GrB_SUCCESS);
+                        if info != GrB_Info::GrB_SUCCESS {
+                            // Handle error appropriately, e.g.:
+                            log::error!("GraphBLAS iterator operation failed: {:?}", info);
+                            return None;
+                        }

Also applies to: 516-516

Comment on lines +695 to +707
fn syncronize(
matrix_policy: MatrixPolicy,
m: &mut DeltaMatrix,
nrows: u64,
ncols: u64,
) -> &mut DeltaMatrix {
match matrix_policy {
MatrixPolicy::FlushResize => m.synchronize(nrows, ncols),
MatrixPolicy::Resize => m.resize(nrows, ncols),
_ => (),
};
m
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for matrix operations in synchronize

The synchronize method performs matrix operations without error handling. These operations could fail, especially in low-memory conditions.

fn syncronize(
    matrix_policy: MatrixPolicy,
    m: &mut DeltaMatrix,
    nrows: u64,
    ncols: u64,
-) -> &mut DeltaMatrix {
+) -> Result<&mut DeltaMatrix, &'static str> {
    match matrix_policy {
-       MatrixPolicy::FlushResize => m.synchronize(nrows, ncols),
-       MatrixPolicy::Resize => m.resize(nrows, ncols),
+       MatrixPolicy::FlushResize => m.synchronize(nrows, ncols)
+           .map_err(|_| "Failed to synchronize matrix")?,
+       MatrixPolicy::Resize => m.resize(nrows, ncols)
+           .map_err(|_| "Failed to resize matrix")?,
        _ => (),
    };
-   m
+   Ok(m)
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn syncronize(
matrix_policy: MatrixPolicy,
m: &mut DeltaMatrix,
nrows: u64,
ncols: u64,
) -> &mut DeltaMatrix {
match matrix_policy {
MatrixPolicy::FlushResize => m.synchronize(nrows, ncols),
MatrixPolicy::Resize => m.resize(nrows, ncols),
_ => (),
};
m
}
fn syncronize(
matrix_policy: MatrixPolicy,
m: &mut DeltaMatrix,
nrows: u64,
ncols: u64,
) -> Result<&mut DeltaMatrix, &'static str> {
match matrix_policy {
MatrixPolicy::FlushResize => m.synchronize(nrows, ncols)
.map_err(|_| "Failed to synchronize matrix")?,
MatrixPolicy::Resize => m.resize(nrows, ncols)
.map_err(|_| "Failed to resize matrix")?,
_ => (),
};
Ok(m)
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Outside diff range and nitpick comments (3)
src/binding/crwlock.rs (1)

16-20: Document deadlock prevention strategy

The struct needs documentation explaining how it prevents deadlocks, especially when acquiring multiple locks.

Add documentation like:

/// CRWLock provides read-write locking for graph operations.
/// 
/// # Deadlock Prevention
/// 
/// To prevent deadlocks:
/// 1. Locks are always acquired in a consistent order
/// 2. No recursive locking is allowed
/// 3. Lock timeout mechanism is implemented
pub struct CRWLock {
    pub rwlock: RwLock<()>,
}
src/graph/matrix/delta_matrix.rs (2)

Line range hint 277-302: Consider using RAII for GraphBLAS resources

The scalar allocation and deallocation could leak resources on panic. Consider using RAII:

struct GrBScalar {
    scalar: GrB_Scalar,
}

impl Drop for GrBScalar {
    fn drop(&mut self) {
        unsafe {
            GrB_Scalar_free(&mut self.scalar);
        }
    }
}

Line range hint 489-501: Add documentation for synchronization strategy

The sync method's logic is complex and needs documentation explaining the strategy.

Add documentation like:

/// Synchronizes the matrix state based on conditions:
/// 1. force_sync: Immediately applies all pending changes
/// 2. delta_max_pending_changes: Applies changes when deltas exceed threshold
/// 
/// # Arguments
/// * force_sync - Force immediate synchronization
/// * delta_max_pending_changes - Maximum pending changes before auto-sync
fn sync(&mut self, force_sync: bool, delta_max_pending_changes: u64)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between defa836 and 8e87c9f.

📒 Files selected for processing (5)
  • Cargo.toml (1 hunks)
  • src/binding/crwlock.rs (1 hunks)
  • src/binding/mod.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
  • src/graph/matrix/delta_matrix.rs (32 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/binding/mod.rs
🔇 Additional comments (7)
Cargo.toml (1)

14-14: Consider using the latest stable version of parking_lot

The current version (0.12.3) seems to be ahead of the latest stable release. Consider using version 0.12.1 unless there's a specific requirement for 0.12.3.

src/binding/crwlock.rs (3)

31-37: ⚠️ Potential issue

Add error handling for lock acquisition

The acquire_read method should handle potential lock acquisition failures.


39-45: ⚠️ Potential issue

Add error handling for write lock acquisition

The acquire_write method should handle potential lock acquisition failures.


47-51: ⚠️ Potential issue

Add error handling for lock release

The release method should handle potential unlock failures.

src/graph/matrix/delta_matrix.rs (2)

86-90: ⚠️ Potential issue

Handle None case in transpose access

The unwrap on transpose access could panic. Consider proper error handling.


338-342: ⚠️ Potential issue

Improve error handling in mxm method

The transpose access could panic if None. Consider proper error handling:

-            let n = if desc == GrB_DESC_T1 && n.transpose.is_some() {
-                n.transpose.as_ref().unwrap()
-            } else {
-                &n.matrix
-            };
+            let n = match (desc == GrB_DESC_T1, &n.transpose) {
+                (true, Some(t)) => t,
+                _ => &n.matrix
+            };

Likely invalid or redundant comment.

src/graph/graph.rs (1)

944-964: ⚠️ Potential issue

Fix potential stack overflow and add error handling in EdgeIterator::next

The current implementation has several issues:

  1. Uses recursion that could lead to stack overflow with many relation types
  2. No error handling for get_edge
  3. Missing validation of retrieved edge data

Consider this safer implementation:

-pub fn next(&mut self, e: &mut Edge) -> bool {
+pub fn next(&mut self, e: &mut Edge) -> Result<bool, &'static str> {
-    if let Some((src_id, dest_id, edge_id)) = self.it.next() {
-        e.src_id = src_id;
-        e.dest_id = dest_id;
-        e.relation_id = self.r;
-        self.graph.get_edge(edge_id, e);
-        return true;
-    }
-
-    if self.all_edges && self.r + 1 < self.graph.relation_type_count() {
-        self.r += 1;
-        self.graph.get_relation_matrix(self.r);
-        self.it = self.graph.relations[self.r as usize].iter(self.src_id, self.dest_id);
-        return self.next(e);
-    }
-    false
+    loop {
+        if let Some((src_id, dest_id, edge_id)) = self.it.next() {
+            e.src_id = src_id;
+            e.dest_id = dest_id;
+            e.relation_id = self.r;
+            if !self.graph.get_edge(edge_id, e) {
+                return Err("Failed to get edge");
+            }
+            return Ok(true);
+        }
+
+        if !self.all_edges || self.r + 1 >= self.graph.relation_type_count() {
+            return Ok(false);
+        }
+
+        self.r += 1;
+        self.graph.get_relation_matrix(self.r);
+        self.it = self.graph.relations[self.r as usize].iter(self.src_id, self.dest_id);
+    }
 }

Likely invalid or redundant comment.

Comment on lines +432 to +440
pub fn export(
&self,
transpose: bool,
) -> SparseMatrix {
let s = if transpose {
self.transpose.as_ref().unwrap()
} else {
m.transpose(None, null_mut(), Some(&self.matrix), unsafe {
GrB_DESC_RT0
&self.matrix
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling to export method

The export method could panic on transpose access.

-        let s = if transpose {
-            self.transpose.as_ref().unwrap()
-        } else {
-            &self.matrix
-        };
+        let s = match (transpose, &self.transpose) {
+            (true, Some(t)) => t,
+            (true, None) => return Err(Error::NoTranspose),
+            (false, _) => &self.matrix
+        };

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1008 to +1036
pub fn next(&mut self) -> Option<Edge> {
if self.depleted {
return None;
}
loop {
if let Some((src_id, dest_id, edge_id, _)) = self.it.next() {
let mut e = Edge {
src_id: src_id,
dest_id: dest_id,
id: 0,
attributes: null_mut(),
relation_id: self.r,
relationship: null_mut(),
};
self.graph.get_edge(edge_id, &mut e);
return Some(e);
}
if self.both_dir && self.dir == GraphEdgeDir::Outgoing {
self.dir = GraphEdgeDir::Incoming;
self.it = self.graph.relations[self.r as usize].iter_range(
self.node_id,
self.node_id,
true,
);
continue;
}
return None;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling and improve safety in NodeEdgeIterator::next

The current implementation has several issues:

  1. No error handling for get_edge
  2. Returns edge with potentially null attributes
  3. Complex control flow could be simplified

Consider this safer implementation:

-pub fn next(&mut self) -> Option<Edge> {
+pub fn next(&mut self) -> Result<Option<Edge>, &'static str> {
     if self.depleted {
-        return None;
+        return Ok(None);
     }
     loop {
         if let Some((src_id, dest_id, edge_id, _)) = self.it.next() {
             let mut e = Edge {
                 src_id,
                 dest_id,
                 id: 0,
                 attributes: null_mut(),
                 relation_id: self.r,
                 relationship: null_mut(),
             };
-            self.graph.get_edge(edge_id, &mut e);
-            return Some(e);
+            if !self.graph.get_edge(edge_id, &mut e) {
+                return Err("Failed to get edge");
+            }
+            if e.attributes.is_null() {
+                return Err("Edge has null attributes");
+            }
+            return Ok(Some(e));
         }
         if self.both_dir && self.dir == GraphEdgeDir::Outgoing {
             self.dir = GraphEdgeDir::Incoming;
             self.it = self.graph.relations[self.r as usize].iter_range(
                 self.node_id,
                 self.node_id,
                 true,
             );
             continue;
         }
-        return None;
+        return Ok(None);
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn next(&mut self) -> Option<Edge> {
if self.depleted {
return None;
}
loop {
if let Some((src_id, dest_id, edge_id, _)) = self.it.next() {
let mut e = Edge {
src_id: src_id,
dest_id: dest_id,
id: 0,
attributes: null_mut(),
relation_id: self.r,
relationship: null_mut(),
};
self.graph.get_edge(edge_id, &mut e);
return Some(e);
}
if self.both_dir && self.dir == GraphEdgeDir::Outgoing {
self.dir = GraphEdgeDir::Incoming;
self.it = self.graph.relations[self.r as usize].iter_range(
self.node_id,
self.node_id,
true,
);
continue;
}
return None;
}
}
pub fn next(&mut self) -> Result<Option<Edge>, &'static str> {
if self.depleted {
return Ok(None);
}
loop {
if let Some((src_id, dest_id, edge_id, _)) = self.it.next() {
let mut e = Edge {
src_id,
dest_id,
id: 0,
attributes: null_mut(),
relation_id: self.r,
relationship: null_mut(),
};
if !self.graph.get_edge(edge_id, &mut e) {
return Err("Failed to get edge");
}
if e.attributes.is_null() {
return Err("Edge has null attributes");
}
return Ok(Some(e));
}
if self.both_dir && self.dir == GraphEdgeDir::Outgoing {
self.dir = GraphEdgeDir::Incoming;
self.it = self.graph.relations[self.r as usize].iter_range(
self.node_id,
self.node_id,
true,
);
continue;
}
return Ok(None);
}
}

Comment on lines +875 to +910
impl Drop for Graph {
fn drop(&mut self) {
unsafe {
let it = if self.partial {
DataBlock_FullScan(self.nodes)
} else {
self.scan_nodes()
};
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);

let it = if self.partial {
DataBlock_FullScan(self.edges)
} else {
self.scan_edges()
};
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);

DataBlock_Free(self.nodes);
DataBlock_Free(self.edges);
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety documentation and error handling in Drop implementation

The Drop implementation contains unsafe code without proper documentation and error handling.

Consider this improved implementation:

 impl Drop for Graph {
     fn drop(&mut self) {
         unsafe {
+            // SAFETY: These FFI calls are safe because:
+            // 1. self.nodes and self.edges are valid pointers from Graph::new
+            // 2. DataBlock_FullScan/DataBlock_Scan return valid iterators
+            // 3. We properly free all resources in the correct order
+
+            // Free node attributes
             let it = if self.partial {
                 DataBlock_FullScan(self.nodes)
             } else {
                 self.scan_nodes()
             };
+            if it.is_null() {
+                // Log error but continue cleanup
+                eprintln!("Warning: Failed to create node iterator during cleanup");
+            }
             loop {
                 let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
                 if set.is_null() {
                     break;
                 }
+                // SAFETY: set is a valid pointer from DataBlockIterator_Next
                 AttributeSet_Free(set);
             }
             DataBlockIterator_Free(it);

             // Similar improvements for edge cleanup...

+            // SAFETY: These are the original pointers from Graph::new
             DataBlock_Free(self.nodes);
             DataBlock_Free(self.edges);
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
impl Drop for Graph {
fn drop(&mut self) {
unsafe {
let it = if self.partial {
DataBlock_FullScan(self.nodes)
} else {
self.scan_nodes()
};
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);
let it = if self.partial {
DataBlock_FullScan(self.edges)
} else {
self.scan_edges()
};
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);
DataBlock_Free(self.nodes);
DataBlock_Free(self.edges);
}
}
}
impl Drop for Graph {
fn drop(&mut self) {
unsafe {
// SAFETY: These FFI calls are safe because:
// 1. self.nodes and self.edges are valid pointers from Graph::new
// 2. DataBlock_FullScan/DataBlock_Scan return valid iterators
// 3. We properly free all resources in the correct order
// Free node attributes
let it = if self.partial {
DataBlock_FullScan(self.nodes)
} else {
self.scan_nodes()
};
if it.is_null() {
// Log error but continue cleanup
eprintln!("Warning: Failed to create node iterator during cleanup");
}
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
// SAFETY: set is a valid pointer from DataBlockIterator_Next
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);
let it = if self.partial {
DataBlock_FullScan(self.edges)
} else {
self.scan_edges()
};
loop {
let set = DataBlockIterator_Next(it, null_mut()) as *mut AttributeSet;
if set.is_null() {
break;
}
AttributeSet_Free(set);
}
DataBlockIterator_Free(it);
// SAFETY: These are the original pointers from Graph::new
DataBlock_Free(self.nodes);
DataBlock_Free(self.edges);
}
}
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 8e87c9f and b8a4bfd.

📒 Files selected for processing (3)
  • src/binding/crwlock.rs (1 hunks)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)

Comment on lines 10 to 13
thread_local! {
static READ_GUARD: Cell<Option<RwLockReadGuard<'static, ()>>> = Cell::new(None);
static WRITE_GUARD: Cell<Option<RwLockWriteGuard<'static, ()>>> = Cell::new(None);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider potential memory leaks with thread_local storage.

The thread_local storage of lock guards could lead to memory leaks if threads terminate without calling release(). Consider using RAII patterns with a guard type that implements Drop to ensure automatic cleanup.

pub struct CRWLockGuard<'a> {
    lock: &'a CRWLock,
}

impl<'a> Drop for CRWLockGuard<'a> {
    fn drop(&mut self) {
        self.lock.release();
    }
}

Comment on lines +21 to +25
unsafe impl Sync for CRWLock {}

impl CRWLock {
pub fn new() -> Self {
CRWLock {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add deadlock prevention mechanism.

The current implementation lacks deadlock prevention. If a thread panics while holding a lock, other threads may deadlock.

impl CRWLock {
    pub fn acquire_read(&self) -> Result<CRWLockGuard, &'static str> {
        match catch_unwind(|| self.rwlock.read()) {
            Ok(guard) => {
                READ_GUARD.with(|cell| 
                    cell.set(Some(unsafe { std::mem::transmute(guard) })));
                Ok(CRWLockGuard { lock: self })
            }
            Err(_) => Err("Thread panicked while acquiring read lock")
        }
    }
}

Comment on lines 33 to 35
unsafe {
READ_GUARD.set(Some(std::mem::transmute(guard)));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unsafe lifetime transmute needs justification.

The transmute of lock guard lifetimes is dangerous and could lead to use-after-free bugs.

Consider using a safer approach:

thread_local! {
    static READ_GUARD: RefCell<Option<RwLockReadGuard<'static, ()>>> = RefCell::new(None);
}

Comment on lines 46 to 50
#[inline]
pub fn release(&self) {
WRITE_GUARD.take();
READ_GUARD.take();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for release operation.

The release method should handle potential errors that may occur during unlock operations.

pub fn release(&self) -> Result<(), &'static str> {
    READ_GUARD.with(|cell| cell.take());
    WRITE_GUARD.with(|cell| cell.take());
    Ok(())
}

src/graph/ffi.rs Outdated
Comment on lines 21 to 37
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AcquireReadLock(g: *mut Graph) {
(&mut *g).acquire_read_lock();
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AcquireWriteLock(g: *mut Graph) {
(&mut *g).acquire_write_lock();
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ReleaseLock(g: *mut Graph) {
(&mut *g).release_lock();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks and error handling in lock management functions.

The lock management functions lack null pointer checks and error handling.

unsafe extern "C" fn Graph_AcquireReadLock(g: *mut Graph) -> bool {
    if g.is_null() {
        return false;
    }
    match (&mut *g).acquire_read_lock() {
        Ok(_) => true,
        Err(_) => false
    }
}

Comment on lines +638 to +654
pub fn get_node_labels(
&mut self,
n: &Node,
labels: &mut [i32],
) -> u32 {
let m = self.get_node_label_matrix();

let mut it = DeltaMatrixIter::new_range(m, n.id, n.id, false);
let mut i = 0;

while let Ok(Some((_, j))) = it.next_bool() {
labels[i as usize] = j as i32;
i += 1;
}

i
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add bounds checking in get_node_labels.

The function writes to labels slice without checking its capacity.

pub fn get_node_labels(&mut self, n: &Node, labels: &mut [i32]) -> Result<u32, &'static str> {
    let m = self.get_node_label_matrix();
    let mut it = DeltaMatrixIter::new_range(m, n.id, n.id, false);
    let mut i = 0u32;

    while let Ok(Some((_, j))) = it.next_bool() {
        if (i as usize) >= labels.len() {
            return Err("Labels buffer too small");
        }
        labels[i as usize] = j as i32;
        i += 1;
    }
    Ok(i)
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (4)
src/graph/graph.rs (4)

6-38: Consider adding documentation for imported types and modules

The imports are well-organized but lack documentation comments explaining the purpose of key types and modules, particularly the custom ones like CRWLock, AttributeSet, etc. This would help developers understand the codebase better.


42-49: Add documentation for public enum MatrixPolicy

This public enum lacks documentation explaining its purpose and when each variant should be used. Consider adding doc comments.

Add documentation like this:

/// Policy for matrix operations in the graph.
#[repr(C)]
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub enum MatrixPolicy {
    /// Flush pending changes and resize the matrix
    FlushResize,
    /// Only resize the matrix
    Resize,
    /// No operation
    Nop,
}

105-119: Add documentation for Graph struct fields

The Graph struct lacks documentation explaining the purpose of each field and their invariants.

Add documentation like this:

/// Represents a graph data structure with nodes, edges, and their relationships.
pub struct Graph {
    /// Number of nodes reserved but not yet created
    reserved_node_count: i32,
    /// Storage for node data
    nodes: *mut DataBlock,
    /// Storage for edge data
    edges: *mut DataBlock,
    // ... document other fields
}

1044-1228: Enhance test coverage for error cases

While the tests cover basic functionality, they don't test error cases such as:

  • Failed allocations
  • Integer overflows
  • Invalid inputs

Consider adding tests for error cases:

#[test]
fn test_create_node_error_cases() {
    test_init();
    let mut g = Graph::new(1, 1);
    g.acquire_write_lock();
    
    // Test overflow
    for _ in 0..std::u64::MAX {
        let mut n = g.reserve_node();
        if let Err(e) = g.create_node(&mut n, &[]) {
            assert_eq!(e, "Failed to allocate node attributes");
            break;
        }
    }
    
    // Test invalid labels
    let mut n = g.reserve_node();
    assert!(g.create_node(&mut n, &[std::i32::MAX]).is_err());
    
    g.release_lock();
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between b8a4bfd and 39d1cdb.

📒 Files selected for processing (1)
  • src/graph/graph.rs (1 hunks)

Comment on lines +59 to +103
struct GraphStatistics {
node_count: Vec<u64>,
edge_count: Vec<u64>,
}
impl GraphStatistics {
fn introduce_label(&mut self) {
self.node_count.push(0);
}

fn increment_node_count(
&mut self,
l: LabelID,
arg: u64,
) {
self.node_count[l as usize] += arg;
}

fn decrement_node_count(
&mut self,
l: LabelID,
arg: u64,
) {
self.node_count[l as usize] -= arg;
}

fn introduce_relationship(&mut self) {
self.edge_count.push(0);
}

fn increment_edge_count(
&mut self,
r: RelationID,
arg: u64,
) {
self.edge_count[r as usize] += arg;
}

fn decrement_edge_count(
&mut self,
r: RelationID,
arg: u64,
) {
self.edge_count[r as usize] -= arg;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add bounds checking and overflow protection to GraphStatistics methods

The methods performing arithmetic operations on vectors lack bounds checking and overflow protection:

  1. Vector access without bounds checking could panic
  2. Arithmetic operations could overflow

Consider this safer implementation:

impl GraphStatistics {
    fn increment_node_count(&mut self, l: LabelID, arg: u64) -> Result<(), &'static str> {
        let count = self.node_count.get_mut(l as usize)
            .ok_or("Invalid label ID")?;
        *count = count.checked_add(arg)
            .ok_or("Node count overflow")?;
        Ok(())
    }

    fn decrement_node_count(&mut self, l: LabelID, arg: u64) -> Result<(), &'static str> {
        let count = self.node_count.get_mut(l as usize)
            .ok_or("Invalid label ID")?;
        *count = count.checked_sub(arg)
            .ok_or("Node count underflow")?;
        Ok(())
    }
}

Comment on lines 122 to 160
pub fn new(
node_cap: u64,
edge_cap: u64,
) -> Self {
let nodes = unsafe {
DataBlock_New(
node_cap,
node_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
let edges = unsafe {
DataBlock_New(
edge_cap,
edge_cap,
size_of::<AttributeSet>() as u32,
transmute_copy(&(AttributeSet_Free as *const c_void)),
)
};
Self {
reserved_node_count: 0,
nodes: nodes,
edges: edges,
adjacency_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, true),
labels: Vec::new(),
node_labels: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
relations: Vec::new(),
zero_matrix: DeltaMatrix::new(unsafe { GrB_BOOL }, node_cap, node_cap, false),
crwlock: CRWLock::new(),
writelocked: false,
matrix_policy: MatrixPolicy::FlushResize,
stats: GraphStatistics {
node_count: Vec::new(),
edge_count: Vec::new(),
},
partial: false,
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for FFI calls in Graph::new

The constructor uses unsafe FFI calls without checking for allocation failures.

Implement proper error handling:

pub fn new(node_cap: u64, edge_cap: u64) -> Result<Self, &'static str> {
    let nodes = unsafe {
        let ptr = DataBlock_New(
            node_cap,
            node_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            return Err("Failed to allocate nodes DataBlock");
        }
        ptr
    };
    
    let edges = unsafe {
        let ptr = DataBlock_New(
            edge_cap,
            edge_cap,
            size_of::<AttributeSet>() as u32,
            transmute_copy(&(AttributeSet_Free as *const c_void)),
        );
        if ptr.is_null() {
            // Clean up previously allocated resources
            DataBlock_Free(nodes);
            return Err("Failed to allocate edges DataBlock");
        }
        ptr
    };
    
    Ok(Self { /* ... */ })
}

Comment on lines +323 to +339
pub fn create_node(
&mut self,
n: &mut Node,
labels: &[LabelID],
) {
let id = n.id;
n.attributes = unsafe { DataBlock_AllocateItem(self.nodes, &mut n.id) } as _;
unsafe { n.attributes.write_unaligned(null_mut()) };

if id != INVALID_ENTITY_ID {
debug_assert!(id == n.id);
self.reserved_node_count -= 1;
debug_assert!(self.reserved_node_count >= 0);
}

self.label_node(n.id, labels);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling and safety checks to create_node

The method has several safety issues:

  1. No error handling for FFI calls
  2. Potential null pointer dereference
  3. Possible integer underflow

Consider this safer implementation:

pub fn create_node(&mut self, n: &mut Node, labels: &[LabelID]) -> Result<(), &'static str> {
    let id = n.id;
    n.attributes = unsafe {
        let ptr = DataBlock_AllocateItem(self.nodes, &mut n.id) as *mut AttributeSet;
        if ptr.is_null() {
            return Err("Failed to allocate node attributes");
        }
        ptr
    };
    
    if id != INVALID_ENTITY_ID {
        debug_assert!(id == n.id);
        self.reserved_node_count = self.reserved_node_count.checked_sub(1)
            .ok_or("Reserved node count underflow")?;
    }
    
    self.label_node(n.id, labels)?;
    Ok(())
}

Comment on lines +949 to +969
pub fn next(
&mut self,
e: &mut Edge,
) -> bool {
if let Some((src_id, dest_id, edge_id)) = self.it.next() {
e.src_id = src_id;
e.dest_id = dest_id;
e.relation_id = self.r;
self.graph.get_edge(edge_id, e);
return true;
}

if self.all_edges && self.r + 1 < self.graph.relation_type_count() {
self.r += 1;
self.graph.get_relation_matrix(self.r);
self.it = self.graph.relations[self.r as usize].iter(self.src_id, self.dest_id);
return self.next(e);
}
false
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential stack overflow in EdgeIterator::next

The recursive implementation could cause stack overflow with many relation types.

Replace recursion with iteration:

pub fn next(&mut self, e: &mut Edge) -> Result<bool, &'static str> {
    loop {
        if let Some((src_id, dest_id, edge_id)) = self.it.next() {
            e.src_id = src_id;
            e.dest_id = dest_id;
            e.relation_id = self.r;
            if !self.graph.get_edge(edge_id, e) {
                return Err("Failed to get edge");
            }
            return Ok(true);
        }
        
        if !self.all_edges || self.r + 1 >= self.graph.relation_type_count() {
            return Ok(false);
        }
        
        self.r += 1;
        self.graph.get_relation_matrix(self.r);
        self.it = self.graph.relations[self.r as usize].iter(self.src_id, self.dest_id);
    }
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (5)
src/binding/crwlock.rs (2)

28-33: ⚠️ Potential issue

Add error handling to lock initialization.

The new() method should handle potential initialization failures.


43-49: ⚠️ Potential issue

Fix unsafe lifetime transmute in acquire_write.

Similar issues as acquire_read:

  1. Unsafe transmute of lifetimes
  2. No error handling
  3. No deadlock prevention
src/graph/matrix/tensor.rs (3)

89-114: ⚠️ Potential issue

Fix memory leak and add error handling in set_element.

The function has two critical issues:

  1. Memory leak: The vector v is not freed in the single_edge branch
  2. Missing error handling for GraphBLAS operations

Apply this fix:

     pub fn set_element(&mut self, src: u64, dest: u64, id: u64) {
         if let Some(current_edge) = self.m.extract_element_u64(src, dest) {
             unsafe {
                 if single_edge(current_edge) {
                     let mut v = MaybeUninit::uninit();
-                    GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
+                    let info = GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
+                    if info != GrB_Info::GrB_SUCCESS {
+                        return;
+                    }
                     let v = v.assume_init();
                     self.m.set_element_u64(set_msb(v as u64), src, dest);
                     GrB_Vector_setElement_BOOL(v, true, current_edge);
                     GrB_Vector_setElement_BOOL(v, true, id);
                     GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
+                    GrB_Vector_free(&mut (v as *mut _));
                 } else {

465-496: ⚠️ Potential issue

Improve error handling in TensorIterator::next.

The iterator implementation has several issues:

  1. Uses unwrap() on Option which could panic
  2. Uses debug assertions which are stripped in release builds
  3. Lacks proper error handling for GraphBLAS operations

Apply this fix:

     fn next(&mut self) -> Option<Self::Item> {
         if self.deleted {
             return None;
         }
         if single_edge(self.edge_id) {
             self.deleted = true;
             Some((self.src_id, self.dest_id, self.edge_id))
         } else {
             if self.eit.is_none() {
                 let v = clear_msb(self.edge_id) as GrB_Vector;
                 let mut it = MaybeUninit::uninit();
                 unsafe {
-                    GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
-                    GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
+                    let info = GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
+                    if info != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
+                    let info = GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0);
+                    if info != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
                 };
                 self.eit = Some(unsafe { it.assume_init() });
             }
             unsafe {
-                let edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
-                let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
+                let Some(it) = self.eit.as_mut() else {
+                    return None;
+                };
+                let edge_id = GxB_Vector_Iterator_getIndex(it);
+                let info = GxB_Vector_Iterator_next(it);

498-564: ⚠️ Potential issue

Replace debug assertions with proper error handling in TensorRangeIterator.

The implementation uses debug assertions which are stripped in release builds, potentially masking errors.

Apply this fix:

     fn next(&mut self) -> Option<Self::Item> {
         if self.eit.is_some() {
             unsafe {
-                self.edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
-                let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
+                let Some(it) = self.eit.as_mut() else {
+                    return None;
+                };
+                self.edge_id = GxB_Vector_Iterator_getIndex(it);
+                let info = GxB_Vector_Iterator_next(it);
                 if info == GrB_Info::GxB_EXHAUSTED {
                     self.eit = None;
                 }
                 return Some((self.src_id, self.dest_id, self.edge_id, true));
             }
         }
         // ... rest of the implementation ...
-                        debug_assert!(info == GrB_Info::GrB_SUCCESS);
+                        if info != GrB_Info::GrB_SUCCESS {
+                            self.eit = None;
+                            return None;
+                        }
🧹 Nitpick comments (2)
src/binding/crwlock.rs (1)

1-57: Consider architectural improvements for safer lock management.

The current implementation could benefit from several architectural improvements:

  1. Use RAII pattern for automatic lock management
  2. Add proper error types and handling strategy
  3. Implement comprehensive testing strategy
  4. Add documentation for unsafe code and invariants

Consider creating these additional types and tests:

// Error handling
#[derive(Debug)]
pub enum LockError {
    Timeout,
    NotLocked,
    PoisonError,
}

// Tests
#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_lock_guard_drop() {
        // Test RAII behavior
    }
    
    #[test]
    fn test_concurrent_access() {
        // Test thread safety
    }
    
    #[test]
    fn test_timeout_handling() {
        // Test lock timeout
    }
}
src/graph/matrix/tensor.rs (1)

27-37: Add documentation for bit manipulation functions.

These utility functions are critical for edge ID manipulation but lack documentation explaining:

  • The significance of the MSB flag
  • When each function should be used
  • The relationship between single edges and vectors

Add documentation like this:

+/// Returns true if the edge ID represents a single edge (MSB is 0).
+/// Used to distinguish between single edges and edge vectors.
 pub fn single_edge(current_edge: EdgeID) -> bool {
     current_edge as u64 & (1u64 << (u64::BITS as usize - 1)) == 0
 }

+/// Sets the Most Significant Bit of the edge ID to mark it as a vector.
 pub fn set_msb(meid: EdgeID) -> u64 {
     meid as u64 | (1u64 << (u64::BITS as usize - 1))
 }

+/// Clears the Most Significant Bit of the edge ID to get the actual vector pointer.
 pub fn clear_msb(meid: EdgeID) -> u64 {
     meid as u64 & !(1u64 << (u64::BITS as usize - 1))
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1caf778 and acaa06a.

📒 Files selected for processing (3)
  • Cargo.toml (1 hunks)
  • src/binding/crwlock.rs (1 hunks)
  • src/graph/matrix/tensor.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml
🔇 Additional comments (1)
src/graph/matrix/tensor.rs (1)

1-564: Verify GraphBLAS resource management across the codebase.

Let's verify the usage of GraphBLAS resources to ensure proper cleanup.

✅ Verification successful

Based on the shell script results, I can now make a final assessment. The code shows a consistent pattern of GraphBLAS vector management:

GraphBLAS vector resources are properly managed

  • All GrB_Vector_new allocations are matched with corresponding GrB_Vector_free calls
  • The Drop implementation for Tensor ensures cleanup of vector resources via _free_vectors unary operator
  • Vector resources are properly freed in error paths (e.g., when converting from single edge to vector representation)
  • The implementation maintains RAII principles through Rust's ownership system

The codebase demonstrates proper resource management practices for GraphBLAS vectors, with no evidence of memory leaks.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for GraphBLAS vector allocations and their corresponding frees
echo "Checking for potential memory leaks in GraphBLAS vector operations..."

# Pattern 1: Vector allocations without corresponding frees
echo "Vector allocations:"
rg "GrB_Vector_new\(" -A 10

# Pattern 2: Vector frees
echo "Vector frees:"
rg "GrB_Vector_free\("

# Pattern 3: Potential error paths where cleanup might be missed
echo "Potential error paths:"
rg "if .* !=" -A 5

Length of output: 7529

Comment on lines 18 to 25
/// Wrap parking_lot rwlock to promote access from C and direct access from Rust.
/// Used to lock the graph.
pub struct CRWLock {
rwlock: RwLock<()>,
guard: ThreadLocal<Cell<Guard>>,
}
unsafe impl Send for CRWLock {}
unsafe impl Sync for CRWLock {}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve memory management and document unsafe implementations.

The current design has several issues:

  1. ThreadLocal storage of guards could lead to memory leaks if threads terminate without calling release()
  2. The unsafe Send/Sync implementations lack documentation explaining their safety guarantees

Consider these improvements:

  1. Use RAII pattern with a guard type:
pub struct CRWLockGuard<'a> {
    lock: &'a CRWLock,
}

impl<'a> Drop for CRWLockGuard<'a> {
    fn drop(&mut self) {
        self.lock.release();
    }
}
  1. Document unsafe implementations:
/// Safety: The underlying RwLock is Send and the guard is managed thread-locally
unsafe impl Send for CRWLock {}
/// Safety: The underlying RwLock is Sync and the guard access is synchronized
unsafe impl Sync for CRWLock {}

Comment on lines 35 to 41
#[inline]
pub fn acquire_read(&self) {
let cell = self.guard.get_or(|| Cell::new(Guard::None));
unsafe {
cell.set(Guard::Read(std::mem::transmute(self.rwlock.read())));
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix unsafe lifetime transmute in acquire_read.

The current implementation has several issues:

  1. Unsafe transmute of lifetimes is dangerous
  2. No error handling for lock acquisition
  3. No deadlock prevention mechanism

Consider this safer implementation:

pub fn acquire_read(&self) -> Result<CRWLockGuard, LockError> {
    match self.rwlock.try_read_for(Duration::from_secs(1)) {
        Some(guard) => {
            let cell = self.guard.get_or(|| Cell::new(Guard::None));
            cell.set(Guard::Read(guard));
            Ok(CRWLockGuard { lock: self })
        }
        None => Err(LockError::Timeout),
    }
}

Comment on lines 51 to 56
#[inline]
pub fn release(&self) {
if let Some(cell) = self.guard.get() {
cell.set(Guard::None);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve release method safety and cleanup.

The current release implementation:

  1. Doesn't ensure proper cleanup of resources
  2. Doesn't handle potential errors during unlock
  3. Could leave the lock in an inconsistent state if the thread panics

Consider this safer implementation:

pub fn release(&self) -> Result<(), LockError> {
    if let Some(cell) = self.guard.get() {
        // Ensure guard is dropped properly
        match cell.replace(Guard::None) {
            Guard::None => Ok(()),
            _ => {
                // Clean up thread local storage
                self.guard.get().map(|c| c.set(Guard::None));
                Ok(())
            }
        }
    } else {
        Err(LockError::NotLocked)
    }
}

Comment on lines 11 to 17
enum Guard {
Read(RwLockReadGuard<'static, ()>),
Write(RwLockWriteGuard<'static, ()>),
None,
}
unsafe impl Send for Guard {}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unsafe static lifetimes from Guard enum.

The use of 'static lifetime for the guard references is dangerous as it bypasses Rust's lifetime checking mechanisms. This could lead to use-after-free bugs if the guard outlives the lock.

Consider using a generic lifetime parameter instead:

-enum Guard {
-    Read(RwLockReadGuard<'static, ()>),
-    Write(RwLockWriteGuard<'static, ()>),
-    None,
-}
+enum Guard<'a> {
+    Read(RwLockReadGuard<'a, ()>),
+    Write(RwLockWriteGuard<'a, ()>),
+    None,
+}

Additionally, document why the Send implementation is safe.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enum Guard {
Read(RwLockReadGuard<'static, ()>),
Write(RwLockWriteGuard<'static, ()>),
None,
}
unsafe impl Send for Guard {}
enum Guard<'a> {
Read(RwLockReadGuard<'a, ()>),
Write(RwLockWriteGuard<'a, ()>),
None,
}
unsafe impl Send for Guard {}

Comment on lines 43 to 77
static mut UNARYOP: GrB_UnaryOp = null_mut();

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn _free_vectors(
z: *mut c_void,
x: *const c_void,
) {
// see if entry is a vector
let x = *(x as *const EdgeID);
if !single_edge(x) {
let mut v = clear_msb(x) as GrB_Vector;
GrB_Vector_free(&mut v);
}
}

impl Drop for Tensor {
fn drop(&mut self) {
unsafe {
if UNARYOP == null_mut() {
grb_check!(GrB_UnaryOp_new(
&mut UNARYOP,
Some(_free_vectors),
GrB_UINT64,
GrB_UINT64
));
}

let m = self.m.m(false).grb_matrix_ref();
grb_check!(GrB_Matrix_apply(m, null_mut(), null_mut(), UNARYOP, m, null_mut()));
let dp = self.m.dp(false).grb_matrix_ref();
grb_check!(GrB_Matrix_apply(dp, null_mut(), null_mut(), UNARYOP, dp, null_mut()));
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential race condition in UNARYOP initialization.

The static mutable UNARYOP could cause race conditions if multiple threads attempt to initialize it simultaneously. Consider using std::sync::Once for safe initialization.

Here's how to fix it:

-static mut UNARYOP: GrB_UnaryOp = null_mut();
+use std::sync::Once;
+static INIT: Once = Once::new();
+static mut UNARYOP: GrB_UnaryOp = null_mut();

 impl Drop for Tensor {
     fn drop(&mut self) {
         unsafe {
-            if UNARYOP == null_mut() {
+            INIT.call_once(|| {
                 grb_check!(GrB_UnaryOp_new(
                     &mut UNARYOP,
                     Some(_free_vectors),
                     GrB_UINT64,
                     GrB_UINT64
                 ));
-            }
+            });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static mut UNARYOP: GrB_UnaryOp = null_mut();
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn _free_vectors(
z: *mut c_void,
x: *const c_void,
) {
// see if entry is a vector
let x = *(x as *const EdgeID);
if !single_edge(x) {
let mut v = clear_msb(x) as GrB_Vector;
GrB_Vector_free(&mut v);
}
}
impl Drop for Tensor {
fn drop(&mut self) {
unsafe {
if UNARYOP == null_mut() {
grb_check!(GrB_UnaryOp_new(
&mut UNARYOP,
Some(_free_vectors),
GrB_UINT64,
GrB_UINT64
));
}
let m = self.m.m(false).grb_matrix_ref();
grb_check!(GrB_Matrix_apply(m, null_mut(), null_mut(), UNARYOP, m, null_mut()));
let dp = self.m.dp(false).grb_matrix_ref();
grb_check!(GrB_Matrix_apply(dp, null_mut(), null_mut(), UNARYOP, dp, null_mut()));
}
}
}
use std::sync::Once;
static INIT: Once = Once::new();
static mut UNARYOP: GrB_UnaryOp = null_mut();
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn _free_vectors(
z: *mut c_void,
x: *const c_void,
) {
// see if entry is a vector
let x = *(x as *const EdgeID);
if !single_edge(x) {
let mut v = clear_msb(x) as GrB_Vector;
GrB_Vector_free(&mut v);
}
}
impl Drop for Tensor {
fn drop(&mut self) {
unsafe {
INIT.call_once(|| {
grb_check!(GrB_UnaryOp_new(
&mut UNARYOP,
Some(_free_vectors),
GrB_UINT64,
GrB_UINT64
));
});
let m = self.m.m(false).grb_matrix_ref();
grb_check!(GrB_Matrix_apply(m, null_mut(), null_mut(), UNARYOP, m, null_mut()));
let dp = self.m.dp(false).grb_matrix_ref();
grb_check!(GrB_Matrix_apply(dp, null_mut(), null_mut(), UNARYOP, dp, null_mut()));
}
}
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (5)
src/graph/matrix/tensor.rs (4)

95-114: ⚠️ Potential issue

Fix memory leak in set_element.

The GrB_Vector created in the if single_edge branch is never freed.

Add vector cleanup:

                     GrB_Vector_setElement_BOOL(v, true, current_edge);
                     GrB_Vector_setElement_BOOL(v, true, id);
                     GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
+                    GrB_Vector_free(&mut v);
                 } else {

172-185: ⚠️ Potential issue

Free allocated GrB_Vector in set_elements.

The GrB_Vector allocated in the else branch is not freed after use.

Add vector cleanup:

                     GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
+                    GrB_Vector_free(&mut v);
                 }

465-496: ⚠️ Potential issue

Improve safety in TensorIterator::next.

The iterator implementation has several safety concerns:

  1. No null pointer checks after iterator attachment
  2. Unchecked GraphBLAS operations
  3. Potential use-after-free when accessing the vector

Add safety checks:

     fn next(&mut self) -> Option<Self::Item> {
         if self.deleted {
             return None;
         }
         if single_edge(self.edge_id) {
             self.deleted = true;
             Some((self.src_id, self.dest_id, self.edge_id))
         } else {
             if self.eit.is_none() {
                 let v = clear_msb(self.edge_id) as GrB_Vector;
                 let mut it = MaybeUninit::uninit();
                 unsafe {
-                    GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
-                    GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
+                    if GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut()) != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
+                    if GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0) != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
                 };
                 self.eit = Some(unsafe { it.assume_init() });
             }
             unsafe {
+                let Some(it) = self.eit.as_mut() else {
+                    return None;
+                };
-                let edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
-                let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
+                let edge_id = GxB_Vector_Iterator_getIndex(it);
+                let info = GxB_Vector_Iterator_next(it);
                 if info == GrB_Info::GxB_EXHAUSTED {
                     self.eit = None;
                     self.deleted = true;
                 }
                 return Some((self.src_id, self.dest_id, edge_id));
             }
         }
     }

532-532: ⚠️ Potential issue

Replace debug assertions with proper error handling.

Debug assertions are used to check GraphBLAS operation results. These will be stripped in release builds.

-                        debug_assert!(info == GrB_Info::GrB_SUCCESS);
+                        if info != GrB_Info::GrB_SUCCESS {
+                            // Handle error appropriately, e.g.:
+                            log::error!("GraphBLAS iterator operation failed: {:?}", info);
+                            return None;
+                        }
src/graph/graph.rs (1)

121-158: ⚠️ Potential issue

Add error handling for FFI calls in Graph::new.

The constructor uses unsafe FFI calls without checking for allocation failures.

Implement proper error handling:

-    pub fn new(node_cap: u64, edge_cap: u64) -> Self {
+    pub fn new(node_cap: u64, edge_cap: u64) -> Result<Self, &'static str> {
         let nodes = unsafe {
+            // SAFETY: DataBlock_New is an FFI call that allocates memory.
+            // It's unsafe because it can return a null pointer if allocation fails.
             let ptr = DataBlock_New(
                 node_cap,
                 node_cap,
                 size_of::<AttributeSet>() as u32,
                 transmute_copy(&(AttributeSet_Free as *const c_void)),
             );
+            if ptr.is_null() {
+                return Err("Failed to allocate nodes DataBlock");
+            }
             ptr
         };
         
         let edges = unsafe {
+            // SAFETY: DataBlock_New is an FFI call that allocates memory.
+            // It's unsafe because it can return a null pointer if allocation fails.
             let ptr = DataBlock_New(
                 edge_cap,
                 edge_cap,
                 size_of::<AttributeSet>() as u32,
                 transmute_copy(&(AttributeSet_Free as *const c_void)),
             );
+            if ptr.is_null() {
+                // Clean up previously allocated resources
+                DataBlock_Free(nodes);
+                return Err("Failed to allocate edges DataBlock");
+            }
             ptr
         };
         
-        Self {
+        Ok(Self {
             reserved_node_count: 0,
             nodes,
             edges,
             // ... rest of the fields ...
-        }
+        })
     }
🧹 Nitpick comments (1)
src/graph/matrix/tensor.rs (1)

27-37: Add documentation for bit manipulation functions.

These utility functions manipulate edge IDs using bit operations, but their purpose and usage aren't immediately clear. Consider adding documentation to explain:

  • The significance of the MSB in edge identification
  • When and why these functions should be used
  • The relationship between single edges and multi-edges
+/// Returns true if the edge ID represents a single edge (MSB is 0)
 pub fn single_edge(current_edge: EdgeID) -> bool {
     current_edge as u64 & (1u64 << (u64::BITS as usize - 1)) == 0
 }

+/// Sets the MSB of the edge ID to mark it as a multi-edge container
 pub fn set_msb(meid: EdgeID) -> u64 {
     meid as u64 | (1u64 << (u64::BITS as usize - 1))
 }

+/// Clears the MSB of the edge ID to get the actual vector pointer
 pub fn clear_msb(meid: EdgeID) -> u64 {
     meid as u64 & !(1u64 << (u64::BITS as usize - 1))
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acaa06a and 53ff995.

📒 Files selected for processing (6)
  • Cargo.toml (1 hunks)
  • build.rs (2 hunks)
  • src/binding/crwlock.rs (1 hunks)
  • src/graph/ffi.rs (1 hunks)
  • src/graph/graph.rs (1 hunks)
  • src/graph/matrix/tensor.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml
🔇 Additional comments (4)
src/binding/crwlock.rs (2)

8-12: ⚠️ Potential issue

Remove unsafe static lifetimes from guard variants.

The use of 'static lifetime for the guard references is unsafe as it bypasses Rust's lifetime checking mechanisms. This could lead to use-after-free bugs if the guard outlives the lock.

Apply this diff to use proper lifetime parameters:

-pub enum CRWGuard {
-    Read(RwLockReadGuard<'static, ()>),
-    Write(RwLockWriteGuard<'static, ()>),
-}
+pub enum CRWGuard<'a> {
+    Read(RwLockReadGuard<'a, ()>),
+    Write(RwLockWriteGuard<'a, ()>),
+}

Likely invalid or redundant comment.


14-20: 🛠️ Refactor suggestion

Document safety guarantees for Send and Sync implementations.

The unsafe implementations of Send and Sync traits need documentation explaining why they are safe.

Add safety documentation:

 /// Wrap parking_lot rwlock to promote access from C and direct access from Rust.
 /// Used to lock the graph.
 pub struct CRWLock {
     rwlock: RwLock<()>,
 }
+/// Safety: The underlying RwLock is Send and can be safely sent between threads
 unsafe impl Send for CRWLock {}
+/// Safety: The underlying RwLock is Sync and can be safely shared between threads
 unsafe impl Sync for CRWLock {}

Likely invalid or redundant comment.

build.rs (1)

11-11: Verify LLVM 19 compatibility.

The addition of LLVM 19 library path needs verification to ensure compatibility with the project's requirements.

Also applies to: 26-26

✅ Verification successful

LLVM 19 library path addition is safe

The build script shows a progressive support for multiple LLVM versions (17, 18, and 19) when using Clang. The addition is consistent with the existing pattern and maintains backward compatibility since:

  1. LLVM paths are only added when CLANG=1
  2. Multiple LLVM version paths are included (17, 18, 19), allowing flexibility in the environment
  3. The change only adds a library search path and doesn't enforce any specific LLVM version requirement
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for LLVM version compatibility issues
echo "Checking for LLVM version requirements..."
if [ -f "README.md" ]; then
    rg -i "llvm.*version|version.*llvm" README.md
fi

Length of output: 163


Script:

#!/bin/bash
# Let's check for LLVM version mentions in source files and build scripts
echo "Checking source files for LLVM version requirements..."
rg -i "llvm.*(version|compatibility)" --type rust

# Check Cargo.toml for dependencies that might indicate LLVM version requirements
echo -e "\nChecking Cargo.toml for LLVM-related dependencies..."
if [ -f "Cargo.toml" ]; then
    rg "llvm" Cargo.toml
fi

# Check for any LLVM-related configuration or version constraints
echo -e "\nChecking for LLVM configuration files..."
fd -t f ".*\.(toml|rs)" -X rg -l "llvm"

Length of output: 484


Script:

#!/bin/bash
# Let's examine the build.rs file to understand the LLVM usage context
echo "Checking build.rs for LLVM configuration..."
cat build.rs

# Also check for any existing LLVM library path configurations
echo -e "\nChecking for existing LLVM library configurations..."
rg -B2 -A2 "LLVM" build.rs

Length of output: 1464

src/graph/matrix/tensor.rs (1)

43-77: ⚠️ Potential issue

Fix thread-safety issue with static mutable UNARYOP.

The static mutable UNARYOP is not thread-safe and could cause race conditions if multiple threads attempt to initialize it simultaneously.

Use std::sync::Once for safe initialization:

-static mut UNARYOP: GrB_UnaryOp = null_mut();
+use std::sync::Once;
+static INIT: Once = Once::new();
+static mut UNARYOP: GrB_UnaryOp = null_mut();

 impl Drop for Tensor {
     fn drop(&mut self) {
         unsafe {
-            if UNARYOP == null_mut() {
+            INIT.call_once(|| {
                 grb_check!(GrB_UnaryOp_new(
                     &mut UNARYOP,
                     Some(_free_vectors),
                     GrB_UINT64,
                     GrB_UINT64
                 ));
-            }
+            });

Likely invalid or redundant comment.

Comment on lines +22 to +37
impl CRWLock {
pub fn new() -> Self {
CRWLock {
rwlock: RwLock::new(()),
}
}

#[inline]
pub fn acquire_read(&self) -> CRWGuard {
CRWGuard::Read(unsafe { std::mem::transmute(self.rwlock.read()) })
}

#[inline]
pub fn acquire_write(&self) -> CRWGuard {
CRWGuard::Write(unsafe { std::mem::transmute(self.rwlock.write()) })
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety in lock operations.

The current implementation has several safety issues:

  1. Unsafe transmute of lifetimes is dangerous
  2. No error handling for lock acquisition
  3. No deadlock prevention mechanism

Consider this safer implementation:

impl CRWLock {
    pub fn new() -> Self {
        CRWLock {
            rwlock: RwLock::new(()),
        }
    }

    pub fn acquire_read(&self) -> Result<CRWGuard<'_>, LockError> {
        match self.rwlock.try_read_for(Duration::from_secs(1)) {
            Some(guard) => Ok(CRWGuard::Read(guard)),
            None => Err(LockError::Timeout),
        }
    }

    pub fn acquire_write(&self) -> Result<CRWGuard<'_>, LockError> {
        match self.rwlock.try_write_for(Duration::from_secs(1)) {
            Some(guard) => Ok(CRWGuard::Write(guard)),
            None => Err(LockError::Timeout),
        }
    }
}

#[derive(Debug)]
pub enum LockError {
    Timeout,
}

Comment on lines +26 to +43
unsafe extern "C" fn Graph_AcquireReadLock(g: *mut Graph) -> CRWGuard {
(&mut *g).acquire_read_lock()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_AcquireWriteLock(g: *mut Graph) -> CRWGuard {
(&mut *g).acquire_write_lock()
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_ReleaseLock(
g: *mut Graph,
guard: CRWGuard,
) {
drop(guard);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null pointer checks and error handling in lock operations.

The lock management functions lack null pointer checks and proper error handling, which could lead to undefined behavior.

Apply these improvements:

unsafe extern "C" fn Graph_AcquireReadLock(g: *mut Graph) -> Option<CRWGuard> {
    if g.is_null() {
        return None;
    }
    match (&mut *g).acquire_read_lock() {
        Ok(guard) => Some(guard),
        Err(_) => None,
    }
}

unsafe extern "C" fn Graph_AcquireWriteLock(g: *mut Graph) -> Option<CRWGuard> {
    if g.is_null() {
        return None;
    }
    match (&mut *g).acquire_write_lock() {
        Ok(guard) => Some(guard),
        Err(_) => None,
    }
}

unsafe extern "C" fn Graph_ReleaseLock(
    g: *mut Graph,
    guard: CRWGuard,
) -> bool {
    if g.is_null() {
        return false;
    }
    drop(guard);
    true
}

Comment on lines +75 to +82
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_New(
node_cap: u64,
edge_cap: u64,
) -> *mut Graph {
Box::into_raw(Box::new(Graph::new(node_cap, edge_cap)))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add allocation failure handling in Graph_New.

The function doesn't handle potential allocation failures, which could lead to undefined behavior.

Apply this improvement:

unsafe extern "C" fn Graph_New(
    node_cap: u64,
    edge_cap: u64,
) -> *mut Graph {
    match Box::try_new(Graph::new(node_cap, edge_cap)) {
        Ok(graph) => Box::into_raw(graph),
        Err(_) => std::ptr::null_mut(),
    }
}

Comment on lines +451 to +462
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_PartialFree(g: *mut Graph) {
(&mut *g).set_partial();
drop(Box::from_raw(g));
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_Free(g: *mut Graph) {
drop(Box::from_raw(g));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add double-free prevention in Graph_Free and Graph_PartialFree.

These functions could lead to double-free issues if called multiple times on the same pointer.

Apply this improvement:

use std::sync::atomic::{AtomicPtr, Ordering};

static GRAPH_PTR: AtomicPtr<Graph> = AtomicPtr::new(std::ptr::null_mut());

unsafe extern "C" fn Graph_Free(g: *mut Graph) -> bool {
    if g.is_null() {
        return false;
    }
    if GRAPH_PTR.compare_exchange(
        g,
        std::ptr::null_mut(),
        Ordering::SeqCst,
        Ordering::SeqCst,
    ).is_ok() {
        drop(Box::from_raw(g));
        true
    } else {
        false
    }
}

Comment on lines +158 to +198
#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_CreateNode(
g: *mut Graph,
n: *mut Node,
labels: *mut LabelID,
label_count: u32,
) {
(&mut *g).create_node(
n.as_mut().unwrap(),
if label_count == 0 {
&[]
} else {
from_raw_parts(labels, label_count as usize)
},
);
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_CreateEdge(
g: *mut Graph,
src: NodeID,
dest: NodeID,
r: RelationID,
e: *mut Edge,
) {
(&mut *g).create_edge(src, dest, r, e.as_mut().unwrap());
}

#[no_mangle]
#[allow(non_snake_case)]
unsafe extern "C" fn Graph_CreateEdges(
g: *mut Graph,
r: RelationID,
edges: *mut *mut Edge,
count: u32,
) {
let edges = from_raw_parts_mut(edges, count as usize);
(&mut *g).create_edges(r, edges);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve safety in node and edge creation functions.

The functions lack null pointer checks and proper error handling.

Apply these improvements:

unsafe extern "C" fn Graph_CreateNode(
    g: *mut Graph,
    n: *mut Node,
    labels: *mut LabelID,
    label_count: u32,
) -> bool {
    if g.is_null() || n.is_null() || (label_count > 0 && labels.is_null()) {
        return false;
    }
    let label_slice = if label_count == 0 {
        &[]
    } else {
        match usize::try_from(label_count) {
            Ok(count) => from_raw_parts(labels, count),
            Err(_) => return false,
        }
    };
    match (&mut *g).create_node(&mut *n, label_slice) {
        Ok(_) => true,
        Err(_) => false,
    }
}

unsafe extern "C" fn Graph_CreateEdge(
    g: *mut Graph,
    src: NodeID,
    dest: NodeID,
    r: RelationID,
    e: *mut Edge,
) -> bool {
    if g.is_null() || e.is_null() {
        return false;
    }
    match (&mut *g).create_edge(src, dest, r, &mut *e) {
        Ok(_) => true,
        Err(_) => false,
    }
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/graph/matrix/tensor.rs (2)

352-371: ⚠️ Potential issue

Add error handling in row_degree method.

The method assumes GraphBLAS operations always succeed and doesn't handle potential errors.

     pub fn row_degree(&self, id: u64) -> u64 {
         let mut degree = 0;
         let mut it = DeltaMatrixIter::new_range(&self.m, id, id, false);
         while let Ok(Some((_, _, id))) = it.next_u64() {
             if single_edge(id) {
                 degree += 1;
             } else {
                 let v = clear_msb(id) as GrB_Vector;
                 let mut nvals = MaybeUninit::uninit();
                 unsafe {
-                    GrB_Vector_nvals(nvals.as_mut_ptr(), v);
-                    degree += nvals.assume_init();
+                    match GrB_Vector_nvals(nvals.as_mut_ptr(), v) {
+                        GrB_Info::GrB_SUCCESS => degree += nvals.assume_init(),
+                        err => {
+                            log::error!("Failed to get vector values: {:?}", err);
+                            return 0;
+                        }
+                    }
                 }
             }
         }
         degree
     }

533-533: ⚠️ Potential issue

Replace debug assertions with proper error handling.

Debug assertions are used to check GraphBLAS operation results. These should be replaced with proper error handling as they will be stripped in release builds.

-                        debug_assert!(info == GrB_Info::GrB_SUCCESS);
+                        if info != GrB_Info::GrB_SUCCESS {
+                            log::error!("GraphBLAS iterator operation failed: {:?}", info);
+                            return None;
+                        }
🧹 Nitpick comments (3)
src/graph/matrix/tensor.rs (3)

27-37: Add documentation for bit manipulation functions.

These utility functions are crucial for edge management but lack documentation explaining their purpose and behavior. Consider adding doc comments to explain:

  • What constitutes a "single edge"
  • The significance of the MSB in edge representation
  • When and why these functions should be used
+/// Returns true if the edge ID represents a single edge (MSB is not set).
 pub fn single_edge(current_edge: EdgeID) -> bool {
     current_edge as u64 & (1u64 << (u64::BITS as usize - 1)) == 0
 }

+/// Sets the most significant bit of the edge ID to mark it as a vector of edges.
 pub fn set_msb(meid: EdgeID) -> u64 {
     meid as u64 | (1u64 << (u64::BITS as usize - 1))
 }

+/// Clears the most significant bit of the edge ID to get the underlying vector pointer.
 pub fn clear_msb(meid: EdgeID) -> u64 {
     meid as u64 & !(1u64 << (u64::BITS as usize - 1))
 }

39-78: Document the Tensor struct and its cleanup behavior.

The Tensor struct and its Drop implementation handle complex resource management but lack documentation explaining:

  • The purpose and lifecycle of the Tensor
  • Why UNARYOP is needed and how it's used
  • The cleanup process in the Drop implementation
+/// A tensor structure that manages a graph's edge data using a DeltaMatrix.
+/// It supports both single edges and vectors of edges, using the MSB of edge IDs
+/// to distinguish between them.
 pub struct Tensor {
     pub m: DeltaMatrix,
 }

 static INIT: Once = Once::new();
 static mut UNARYOP: GrB_UnaryOp = null_mut();

+/// Callback function used during cleanup to free any vectors stored in the matrix.
+/// This is called for each non-zero element in the matrix during Drop.
 #[no_mangle]
 #[allow(non_snake_case)]
 unsafe extern "C" fn _free_vectors(

1-565: Consider adding comprehensive documentation and error handling strategy.

The codebase would benefit from:

  1. Module-level documentation explaining the tensor implementation strategy
  2. Error type definitions for GraphBLAS operations
  3. Consistent error handling patterns across all methods
  4. Documentation of safety requirements for unsafe blocks
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53ff995 and 77d864b.

📒 Files selected for processing (1)
  • src/graph/matrix/tensor.rs (1 hunks)
🔇 Additional comments (2)
src/graph/matrix/tensor.rs (2)

90-115: ⚠️ Potential issue

Fix memory and error handling issues in set_element.

The method has several issues:

  1. No error handling for GraphBLAS operations
  2. Memory leak in the vector allocation path
  3. Unsafe block is too large
     pub fn set_element(&mut self, src: u64, dest: u64, id: u64) {
         if let Some(current_edge) = self.m.extract_element_u64(src, dest) {
             unsafe {
                 if single_edge(current_edge) {
                     let mut v = MaybeUninit::uninit();
-                    GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
-                    let v = v.assume_init();
+                    let info = GrB_Vector_new(v.as_mut_ptr(), GrB_BOOL, GrB_INDEX_MAX);
+                    if info != GrB_Info::GrB_SUCCESS {
+                        return;
+                    }
+                    let mut v = v.assume_init();
                     self.m.set_element_u64(set_msb(v as u64), src, dest);
-                    GrB_Vector_setElement_BOOL(v, true, current_edge);
-                    GrB_Vector_setElement_BOOL(v, true, id);
-                    GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
+                    if let Err(_) = unsafe {
+                        GrB_Vector_setElement_BOOL(v, true, current_edge);
+                        GrB_Vector_setElement_BOOL(v, true, id);
+                        GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE)
+                    } {
+                        GrB_Vector_free(&mut v);
+                        return;
+                    }
                 } else {
                     let v = clear_msb(current_edge) as GrB_Vector;
                     GrB_Vector_setElement_BOOL(v, true, id);
                     GrB_Vector_wait(v, GrB_WaitMode::GrB_MATERIALIZE);
                 }
             }
         } else {
             self.m.set_element_u64(id, src, dest);
         }
     }

Likely invalid or redundant comment.


466-497: ⚠️ Potential issue

Improve safety in TensorIterator implementation.

The iterator implementation has several safety concerns:

  1. Unchecked GraphBLAS operations
  2. Potential null pointer dereference
  3. No cleanup of resources on error
     fn next(&mut self) -> Option<Self::Item> {
         if self.deleted {
             return None;
         }
         if single_edge(self.edge_id) {
             self.deleted = true;
             Some((self.src_id, self.dest_id, self.edge_id))
         } else {
             if self.eit.is_none() {
                 let v = clear_msb(self.edge_id) as GrB_Vector;
                 let mut it = MaybeUninit::uninit();
                 unsafe {
-                    GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut());
-                    GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0)
+                    if GxB_Vector_Iterator_attach(it.as_mut_ptr(), v, null_mut()) != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
+                    if GxB_Vector_Iterator_seek(it.as_mut_ptr(), 0) != GrB_Info::GrB_SUCCESS {
+                        self.deleted = true;
+                        return None;
+                    }
                 };
                 self.eit = Some(unsafe { it.assume_init() });
             }
             unsafe {
+                let Some(it) = self.eit.as_mut() else {
+                    return None;
+                };
-                let edge_id = GxB_Vector_Iterator_getIndex(self.eit.as_mut().unwrap());
-                let info = GxB_Vector_Iterator_next(self.eit.as_mut().unwrap());
+                let edge_id = GxB_Vector_Iterator_getIndex(it);
+                let info = GxB_Vector_Iterator_next(it);
                 if info == GrB_Info::GxB_EXHAUSTED {
                     self.eit = None;
                     self.deleted = true;
                 }
                 return Some((self.src_id, self.dest_id, edge_id));
             }
         }
     }

Likely invalid or redundant comment.

@@ -17,6 +23,7 @@ fn main() {
println!("cargo:rustc-link-arg=-L/opt/homebrew/opt/libomp/lib");
Copy link

Choose a reason for hiding this comment

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

Why do we have duplication of the link flags ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what duplication?

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.

2 participants