diff --git a/bin/ci.sh b/bin/ci.sh index 9622371..86e3679 100755 --- a/bin/ci.sh +++ b/bin/ci.sh @@ -10,3 +10,5 @@ wasm-pack build --target nodejs --release echo "" echo "Adding @urbdyn/ to package.json ..." sed -i.bak 's|"name": "petgraph-wasm",|"name": "@urbdyn/petgraph-wasm",|g' "$repo_dir/pkg/package.json" +echo "Adding iterators to JS classes ..." +$repo_dir/bin/patch_compiled_output.js diff --git a/bin/patch_compiled_output.js b/bin/patch_compiled_output.js new file mode 100755 index 0000000..223bdca --- /dev/null +++ b/bin/patch_compiled_output.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +/** + * This file is used to replace a particular error that occurs when ncc + * compiles oracledb code. More details on this issue can be found at: + * https://github.com/vercel/ncc/issues/205 + */ + +const fs = require('fs') +const path = require('path') + +// File to make swap on +const targetFilePath = path.join(__dirname, '../pkg/petgraph_wasm.js') +const replacements = [ + [path.join(__dirname, 'replacement_1_in.txt'), path.join(__dirname, 'replacement_1_out.txt')], + [path.join(__dirname, 'replacement_2_in.txt'), path.join(__dirname, 'replacement_2_out.txt')], +] + +function main() { + console.log('Starting patch_compiled_output.js ...') + + console.log(`targetFilePath = ${targetFilePath}`) + const targetFileText = fs.readFileSync(targetFilePath).toString() + + let newFileText = targetFileText + for ([textInFile, textOutFile] of replacements) { + const textIn = fs.readFileSync(textInFile).toString() + const textOut = fs.readFileSync(textOutFile).toString() + + console.log(`textInFile = ${textInFile}`) + console.log(`textOutFile = ${textOutFile}`) + + newFileText = newFileText.replace(textIn, textOut) + } + + if (newFileText !== targetFileText) { + console.log('Making replacement!') + // Update file with fixes + fs.writeFileSync(targetFilePath, newFileText) + } else { + console.log('No replacements made.') + } + console.log('Ending patch_compiled_output.js\n') +} + +main() diff --git a/bin/replacement_1_in.txt b/bin/replacement_1_in.txt new file mode 100644 index 0000000..823f2c8 --- /dev/null +++ b/bin/replacement_1_in.txt @@ -0,0 +1,3 @@ +class SccGroups { + + static __wrap(ptr) { \ No newline at end of file diff --git a/bin/replacement_1_out.txt b/bin/replacement_1_out.txt new file mode 100644 index 0000000..f873526 --- /dev/null +++ b/bin/replacement_1_out.txt @@ -0,0 +1,13 @@ +class SccGroups { + // NOTE: THIS IS ADDED AT BUILD TIME AFTER COMPILATION. + [Symbol.iterator]() { + let counter = 0; + return { + next: () => { + counter++ + return { value: this.getGroup(counter - 1), done: counter > this.length } + }, + } + } + + static __wrap(ptr) { \ No newline at end of file diff --git a/bin/replacement_2_in.txt b/bin/replacement_2_in.txt new file mode 100644 index 0000000..5eb26d8 --- /dev/null +++ b/bin/replacement_2_in.txt @@ -0,0 +1,3 @@ +class SccGroup { + + static __wrap(ptr) { \ No newline at end of file diff --git a/bin/replacement_2_out.txt b/bin/replacement_2_out.txt new file mode 100644 index 0000000..ccb1e7e --- /dev/null +++ b/bin/replacement_2_out.txt @@ -0,0 +1,13 @@ +class SccGroup { + // NOTE: THIS IS ADDED AT BUILD TIME AFTER COMPILATION. + [Symbol.iterator]() { + let counter = 0; + return { + next: () => { + counter++ + return { value: this.getItem(counter - 1), done: counter > this.length } + }, + } + } + + static __wrap(ptr) { \ No newline at end of file diff --git a/example_js/basic_example.js b/example_js/basic_example.js index 721cf0e..b1b1333 100755 --- a/example_js/basic_example.js +++ b/example_js/basic_example.js @@ -2,12 +2,14 @@ let petgraph = require('../pkg/petgraph_wasm') -let g = petgraph.DirectedGraph.new() +let g = new petgraph.DiGraph() -let cities = ["NYC","Vilnius","Knoxville","Taipei","Buenos Aires"] +const cities = ["NYC","Vilnius","Knoxville","Taipei","Buenos Aires"] +let city_name_to_index = {} cities.forEach((city) => { console.log(`Adding node: ${city}`) - g.add_node(city) + const i = g.addNode(city) + city_name_to_index[city] = i }) let city_pairs = [ @@ -22,16 +24,35 @@ let city_pairs = [ ["Buenos Aires","Taipei"], ] city_pairs.forEach(([src,dest]) => { - const src_index = g.get_node_index(src) - const dest_index = g.get_node_index(dest) console.log(`Adding graph edge: ${src} -> ${dest}`) - g.add_edge(src_index, dest_index, 0) + g.addEdge(city_name_to_index[src], city_name_to_index[dest], 0) }) -console.log("Sorting nodes ...") -const sorted_g = g.get_sorted() +console.log("\nSorting nodes ...") +const sorted_g = petgraph.toposort(g) console.log(sorted_g) sorted_g.forEach((i) => { - console.log(typeof i) - console.log(i) + console.log(`${i} = ${g.nodeWeight(i)}`) }) + +let city_pairs2 = [ + ["Taipei","Buenos Aires"], +] + +console.log("\nAdding city pairs 2 ...") +city_pairs2.forEach(([src,dest]) => { + console.log(`Adding graph edge: ${src} -> ${dest}`) + g.addEdge(city_name_to_index[src], city_name_to_index[dest], 0) +}) + +console.log("\nCreating SCC of nodes (tarjan) ...") + +const scc_g = petgraph.tarjanScc(g) + +console.log("\nIterator test of SCC of nodes (tarjan) ...") +for (scc_g_i of scc_g) { + console.log(scc_g_i) + for (scc_g_i_item of scc_g_i) { + console.log(`${scc_g_i_item} = ${g.nodeWeight(scc_g_i_item)}`) + } +} diff --git a/src/algo/mod.rs b/src/algo/mod.rs index f65cbef..5c66646 100644 --- a/src/algo/mod.rs +++ b/src/algo/mod.rs @@ -1,9 +1,20 @@ use crate::graph_impl::DiGraph; +use crate::js_helpers::scc_groups::SccGroups; use crate::GraphError; use petgraph::algo; use petgraph::graph; use wasm_bindgen::prelude::*; +#[wasm_bindgen(js_name = kosarajuScc)] +pub fn kosaraju_scc(graph: &DiGraph) -> SccGroups { + SccGroups::new(algo::kosaraju_scc(&graph.graph)) +} + +#[wasm_bindgen(js_name = tarjanScc)] +pub fn tarjan_scc(graph: &DiGraph) -> SccGroups { + SccGroups::new(algo::tarjan_scc(&graph.graph)) +} + #[wasm_bindgen] pub fn toposort(graph: &DiGraph) -> Result, JsValue> { match algo::toposort(&graph.graph, None) { @@ -26,6 +37,7 @@ mod tests { use super::*; use crate::js_helpers::test::*; use crate::{GraphError, GraphItemType}; + use graph::NodeIndex; use wasm_bindgen_test::*; #[wasm_bindgen_test] @@ -43,4 +55,46 @@ mod tests { let expect_err = GraphError::new("Cycle detected", GraphItemType::Node, 1); assert_eq!(sort_err.into_serde::().unwrap(), expect_err); } + + #[wasm_bindgen_test] + fn can_get_kosaraju_scc() { + let (mut g, _nodes, _edges) = new_test_graph2(); + g.add_edge(2, 1, JsValue::NULL); + let scc_groups: SccGroups = kosaraju_scc(&g); + assert_eq!( + scc_groups.try_into_vecs().unwrap(), + vec![ + vec![NodeIndex::new(6)], + vec![NodeIndex::new(5)], + vec![ + NodeIndex::new(0), + NodeIndex::new(3), + NodeIndex::new(2), + NodeIndex::new(1) + ], + vec![NodeIndex::new(4)] + ] + ); + } + + #[wasm_bindgen_test] + fn can_get_tarjan_scc() { + let (mut g, _nodes, _edges) = new_test_graph2(); + g.add_edge(2, 1, JsValue::NULL); + let scc_groups: SccGroups = tarjan_scc(&g); + assert_eq!( + scc_groups.try_into_vecs().unwrap(), + vec![ + vec![ + NodeIndex::new(3), + NodeIndex::new(1), + NodeIndex::new(2), + NodeIndex::new(0) + ], + vec![NodeIndex::new(4)], + vec![NodeIndex::new(6)], + vec![NodeIndex::new(5)] + ] + ); + } } diff --git a/src/js_helpers/mod.rs b/src/js_helpers/mod.rs index 6e7b4c4..5cd150a 100644 --- a/src/js_helpers/mod.rs +++ b/src/js_helpers/mod.rs @@ -2,5 +2,6 @@ //! needed in addition to the raw petgraph features so that the library is usable //! from Javascript. +pub mod scc_groups; pub mod test; pub mod vec_tree; diff --git a/src/js_helpers/scc_groups.rs b/src/js_helpers/scc_groups.rs new file mode 100644 index 0000000..889a8f7 --- /dev/null +++ b/src/js_helpers/scc_groups.rs @@ -0,0 +1,153 @@ +use crate::js_helpers::vec_tree::*; + +use petgraph::graph; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Debug)] +pub struct SccGroups { + #[wasm_bindgen(skip)] + pub vec_tree: VecTree, +} + +impl SccGroups { + /// Create SccGroups from 2D Vec of NodeIndex, as returned by SCC algorithms. + pub fn new(data: Vec>) -> Self { + Self { + vec_tree: VecTree::new2d(data, |x| x.index() as usize), + } + } + + /// Attempts to extract the `Vec`s from the inner `vec_tree`, which is inside an `Rc`. + /// If this fails then self is returned. + pub fn try_into_vecs(self) -> Result>, Self> { + match self.vec_tree.try_unwrap() { + Ok(vti) => match vti { + VecTreeInner::Vt2D(vt2d) => Ok(vt2d.into_vecs()), + _ => panic!("SccGroups has wrong dimensional VecTree"), + }, + Err(vec_tree) => Err(Self { vec_tree }), + } + } +} + +#[wasm_bindgen] +impl SccGroups { + #[wasm_bindgen(js_name = getGroup)] + pub fn get_group(&self, index: usize) -> Option { + match self.vec_tree.get(&[index]) { + VecTreeElement::View(Some(view)) => Some(SccGroup { view }), + VecTreeElement::View(None) => None, + VecTreeElement::Item(_) => panic!("SccGroups::get_group returned item"), + } + } + + #[wasm_bindgen(js_name = length, getter)] + pub fn len(&self) -> usize { + match self.vec_tree.get(&[]) { + VecTreeElement::View(Some(view)) => view.len(), + VecTreeElement::View(None) => 0, + VecTreeElement::Item(_) => panic!("SccGroups::len returned item"), + } + } + + #[wasm_bindgen(js_name = isEmpty)] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Create copy of SccGroups as `Array>`. + /// This is a convenience method for workings with SccGroups! + /// Using native Javascript `Array`s is less memory efficient. + /// Calling this function will produce a full copy using nested `Array` + /// meaning this will greatly increase memory consumption. + #[wasm_bindgen(js_name = toArrays)] + pub fn to_arrays(&self) -> js_sys::Array { + match &*self.vec_tree.inner() { + VecTreeInner::Vt2D(vt2d) => vt2d + .data_iter() + .map(|child_vec| { + child_vec + .iter() + .map(|x| JsValue::from(x.index() as u32)) + .collect::() + }) + .collect::(), + _ => panic!("SccGroups has wrong dimensional VecTree"), + } + } +} + +#[wasm_bindgen] +#[derive(Debug)] +pub struct SccGroup { + #[wasm_bindgen(skip)] + pub view: VecTreeView, +} + +#[wasm_bindgen] +impl SccGroup { + #[wasm_bindgen(js_name = getItem)] + pub fn get_item(&self, index: usize) -> Option { + self.view.get_item(index) + } + + #[wasm_bindgen(js_name = length, getter)] + pub fn len(&self) -> usize { + self.view.len() + } + + #[wasm_bindgen(js_name = isEmpty)] + pub fn is_empty(&self) -> bool { + self.view.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use graph::NodeIndex; + use wasm_bindgen_test::*; + + fn new_test_scc_groups() -> SccGroups { + SccGroups::new(vec![ + vec![ + NodeIndex::new(0), + NodeIndex::new(1), + NodeIndex::new(2), + NodeIndex::new(3), + ], + vec![NodeIndex::new(4)], + ]) + } + + #[wasm_bindgen_test] + fn can_access_scc_groups() { + // JS array => [0] + let js_array_0 = js_sys::Array::of1(&JsValue::from(0u8)); + // JsValue of SccGroups1 + let scc_groups = JsValue::from(new_test_scc_groups()); + assert!(scc_groups.is_object()); + // Retrieve the "getGroup" function by name for scc_groups + let scc_groups_get_group_raw: JsValue = + js_sys::Reflect::get(&scc_groups, &JsValue::from_str("getGroup")).unwrap(); + assert!(scc_groups_get_group_raw.is_function()); + let scc_groups_get_group = js_sys::Function::from(scc_groups_get_group_raw); + + // Call scc_groups.getGroup(0) to get first scc_group object + let scc_group0: JsValue = + js_sys::Reflect::apply(&scc_groups_get_group, &scc_groups, &js_array_0).unwrap(); + assert!(scc_group0.is_object()); + // Retrieve the "getItem" function by name for scc_group + let scc_group0_get_item_raw: JsValue = + js_sys::Reflect::get(&scc_group0, &JsValue::from_str("getItem")).unwrap(); + assert!(scc_group0_get_item_raw.is_function()); + let scc_group0_get_item = js_sys::Function::from(scc_group0_get_item_raw); + + // Retrieve the first item from the first group + let scc_group0_item0: JsValue = + js_sys::Reflect::apply(&scc_group0_get_item, &scc_group0, &js_array_0).unwrap(); + + assert_eq!(scc_group0_item0, JsValue::from(0)); + } +} diff --git a/src/js_helpers/test.rs b/src/js_helpers/test.rs index 3a42678..034f2db 100644 --- a/src/js_helpers/test.rs +++ b/src/js_helpers/test.rs @@ -30,3 +30,13 @@ pub fn new_test_graph() -> (DiGraph, Vec, Vec) { ]; (g, nodes, edges) } + +pub fn new_test_graph2() -> (DiGraph, Vec, Vec) { + let (mut g, mut nodes, mut edges) = new_test_graph(); + nodes.extend(vec![ + g.add_node(JsValue::from_str("Denver")), + g.add_node(JsValue::from_str("Amsterdam")), + ]); + edges.extend(vec![g.add_edge(nodes[5], nodes[6], JsValue::NULL)]); + (g, nodes, edges) +}