ascent

repository·master·Indexed 20 days ago

https://github.com/s-arash/ascent

A logic programming language similar to Datalog embedded in Rust via macros. It provides high-performance deductive inference with support for lattices, parallel execution via rayon, and Bring Your Own Data Structures (BYODS) to optimize relations with custom data structures. Features include stratified negation, aggregation, and support for WebAssembly (WASM).

Tokens
21.8K
Snippets
83
Records
101
Agent score
69%

What's inside ascent

  1. Compute fixed points using Lattices

    master

    Ascent supports computing fixed points of user-defined lattices using the lattice keyword. A lattice behaves like a relation, but when a new fact is discovered, the values in the final column are joined together using the Lattice trait implementation. This allows for computations like shortest paths.

    Example using Dual<T> to find shortest paths (where Dual stores the minimum value):

    ascent! {
       lattice shortest_path(i32, i32, Dual<u32>);
       relation edge(i32, i32, u32);
    
       shortest_path(x, y, Dual(*w)) <-- edge(x, y, w);
    
       shortest_path(x, z, Dual(w + l)) <-- 
          edge(x, y, w), 
          shortest_path(y, z, ?Dual(l));
    }
  2. Get started with Bring Your Own Data Structures (BYODS)

    master

    Ascent supports a feature called "Bring Your Own Data Structures" (BYODS), which allows you to provide custom data structure implementations for logic programming tasks.

    To learn how to implement and use custom data structures, refer to the steensgaard example located in the ascent-byods-rels/examples/steensgaard directory.

    See example in: ./ascent-byods-rels/examples/steensgaard
  3. Install and set up Ascent in a Rust project

    master

    Ascent is a logic programming language embedded in Rust via macros. To use it, follow these steps:

    1. Ensure Rust is installed.
    2. Create a new project: cargo new my-ascent-project && cd my-ascent-project.
    3. Add ascent to your Cargo.toml dependencies:
      [dependencies]
      ascent = "*"
    4. Use the ascent! macro in your source code to define relations and rules.
    cargo new my-ascent-project
    cd my-ascent-project
    # Add ascent = "*" to Cargo.toml
  4. Use BYODS to back relations with custom data structures

    master

    BYODS (Bring Your Own Data Structures) allows relations to be backed by optimized custom data structures using the #[ds(...)] attribute. This can significantly improve algorithmic complexity.

    Example using trrel_uf from the ascent-byods-rels crate to optimize transitive closure:

    Dependencies:

    [dependencies]
    ascent-byods-rels = "*"
    ascent = "*"
    use ascent_byods_rels::trrel_uf;
    
    ascent! {
       relation edge(Node, Node);
       
       #[ds(trrel_uf)] // Makes the relation transitive
       relation path(Node, Node);
       
       path(x, y) <-- edge(x, y);
    }
  5. Build and test Ascent on WebAssembly (WASM)

    master

    This project includes a wasm-tests package designed to verify Ascent's functionality when running in a WebAssembly environment. To use this setup, you must use wasm-pack to manage the build and testing lifecycle.

    Setup

    The test environment is generated using the wasm-pack-template. To recreate the environment:

    # Generate the project
    cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
    cd my-project

    Build

    To compile the Rust code into WebAssembly:

    wasm-pack build

    Test

    To run the tests in a headless browser (e.g., Firefox):

    wasm-pack test --headless --firefox

    Dependencies

    This testing environment relies on:

    • wasm-bindgen: Facilitates communication between WebAssembly and JavaScript.
    • console_error_panic_hook: Ensures that Rust panic messages are logged directly to the browser's developer console for easier debugging.
    wasm-pack build
    wasm-pack test --headless --firefox
  6. Read ternary relation indices using TrRelInd0, TrRelInd1, or TrRelIndFull

    master

    Depending on the required access pattern, you can wrap a TrRelIndCommon<T> in different reader types that implement the RelIndexRead and RelIndexReadAll traits:

    • TrRelInd0: Optimized for reading by a single key T. index_get returns an iterator over all y such that (x, y) exists.
    • TrRelInd1: Optimized for reading by the second element (reverse lookup). index_get returns an iterator over all x such that (x, y) exists.
    • TrRelIndFull: Provides full access to the relation. Supports contains_key for checking if a specific (x, y) tuple exists and iter_all to iterate over every tuple in the relation.
    • TrRelIndNone: A fallback reader that iterates over all tuples via a flat map, used when specific indexing is not applicable.
  7. Combine multiple indices using RelIndexCombined

    master

    RelIndexCombined<'a, Ind1, Ind2> is a wrapper that allows you to treat two separate indices as a single unified index. This is useful when a relation is partitioned across multiple index structures.

    Behavior

    • index_get: When searching for a key, it checks both ind1 and ind2. If the key exists in one or both, it returns a chained iterator containing all matching values from both indices.
    • len_estimate: Returns the sum of the length estimates of both indices.
    • is_empty: Returns true only if both underlying indices are empty.
    • iter_all: Returns a chained iterator that traverses all entries in ind1 followed by all entries in ind2.

    Construction

    Use RelIndexCombined::new(ind1, ind2) to create the combined view.

    let combined = RelIndexCombined::new(&index_a, &index_b);
    
    // Searching the combined index
    if let Some(iter) = combined.index_get(&target_key) {
        // This will yield values from both index_a and index_b if they exist
        for val in iter {
            // ...
        }
    }
  8. Iterate over added elements in an equality relation

    master

    To find elements that have been added to an equality relation since its last 'freeze' or state change, you can use the following methods on CEqRelIndCommon<T>:

    • Synchronous: Use iter_all_added() to get an iterator over pairs (&T, &T) that exist in the current combined state but were not present in the old state.
    • Parallel: Use c_iter_all_added() to get a ParallelIterator (via rayon) for high-performance processing of added elements across multiple threads.

    These methods are useful for incremental updates where you only want to process the delta between two states of the relation.

  9. Manage ternary relation indices with TrRelIndCommon

    master

    TrRelIndCommon<T> is a state-managed container for ternary relation indices, used within the BYODS (Bring Your Own Data Structures) framework. It transitions through three states:

    1. New: An empty state used for initial insertions.
    2. Delta: A state representing incremental changes (deltas) applied to a base index.
    3. Total: A state representing a fully consolidated index.

    To use this, you typically start with a New state, perform insertions, and then use the RelIndexMerge trait (specifically merge_delta_to_total_new_to_delta) to consolidate changes into a Total state.

    Key operations:

    • insert(x0, x1): Adds a new relation pair to the index (only works in New state).
    • is_empty(): Checks if the index contains any data.
    • count_exact(): Returns the exact number of elements in the Total index.
    • unwrap_total(): Accesses the underlying TrRelUnionFind<T> (only works in Total state).
    // Example of the state transition logic (conceptual)
    // let mut index = TrRelIndCommon::default(); // Starts as Total (empty)
    // index.insert(val1, val2); // This would panic if not in New state
    // To actually use it, one must manage the transition from New -> Delta -> Total via merge functions.
  10. Convert binary relations to ternary with BinRelToTernary

    master

    The BinRelToTernary struct is used to represent a ternary relation $(T_0, T_1, T_2)$ by mapping a primary key $T_0$ to a binary relation TBinRel between $T_1$ and $T_2$.

    It supports optional reverse indexing for efficient lookups on $T_1$ and $T_2$ via reverse_map1 and reverse_map2 respectively. This is useful when you need to query the relation by any of its three components.

    use ascent::internal::ToRelIndex;
    // T0, T1, T2 must implement Clone + Hash + Eq
    // TBinRel must implement ByodsBinRel<T0 = T1, T1 = T2>
    let ternary_rel = BinRelToTernary::<T0, T1, T2, TBinRel> {
        map: my_hashmap,
        reverse_map1: Some(my_reverse_map1),
        reverse_map2: Some(my_reverse_map2),
    };
  11. Use TrRel2IndCommon for ternary relation indexing

    master

    In the ascent-byods-rels package, TrRel2IndCommon<T0, T1> serves as the underlying storage structure for ternary relations. It uses a HashMap to map a primary key T0 to a relation containing pairs of T1. It can optionally maintain reverse maps (reverse_map1 and reverse_map2) to allow efficient querying by the second or third elements of the ternary relation.

    Key components:

    • map: HashMap<T0, TrRelIndCommon<T1>>
    • reverse_map1: Option<HashMap<T1, AltHashSet<T0>>> (maps T1 to sets of T0)
    • reverse_map2: Option<HashMap<T1, AltHashSet<T0>>> (maps T1 to sets of T0)
  12. Manage lattice-based indices with CLatIndex

    master

    CLatIndex<K, V> is a lattice-based index used for managing mappings from keys (K) to sets of values (V). It is designed to transition between an Unfrozen state (mutable, using DashMap) and a Frozen state (read-only, using dashmap::ReadOnlyView).

    State Transitions

    • Unfrozen: Allows insertions and modifications. Use unwrap_unfrozen() or unwrap_mut_unfrozen() to access the underlying DashMap.
    • Frozen: Provides high-performance read access. Use unwrap_frozen() to access the ReadOnlyView.
    • Freezable Trait: Implement freeze() to transition from Unfrozen to Frozen, and unfreeze() to transition back.

    Key Operations

    • Insertion: Values are inserted into a HashSet associated with the key. If the key doesn't exist, a new set is created.
    • Parallelism: Supports parallel iteration over values via CRelIndexRead and CRelIndexReadAll traits using rayon.
    // Example of conceptual usage (pseudo-code based on API)
    let mut index: CLatIndex<String, i32> = CLatIndex::default();
    
    // Insert values (Unfrozen state)
    index.index_insert("key1".to_string(), 10);
    index.index_insert("key1".to_string(), 20);
    
    // Freeze the index for read-only access
    index.freeze();
    
    // Access frozen view
    let view = index.unwrap_frozen();
    if let Some(values) = view.get("key1") {
        for val in values.iter() {
            println!("{}", val);
        }
    }