Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
[package]
name = "ruvector-dag-wasm"
version = "0.1.0"
edition = "2021"
authors = ["RuVector Contributors"]
description = "Minimal WASM DAG library for browser and embedded systems"
license = "MIT OR Apache-2.0"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bincode = "1.3"
[dependencies.wee_alloc]
version = "0.4"
optional = true
[dev-dependencies]
wasm-bindgen-test = "0.3"
[features]
default = []
# Enable wee_alloc for ~10KB smaller WASM binary
wee_alloc = ["dep:wee_alloc"]
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

View File

@@ -0,0 +1,418 @@
//! Minimal WASM DAG library optimized for browser and embedded systems
//!
//! Size optimizations:
//! - u8/u32/f32 instead of larger types
//! - Inline hot paths
//! - Minimal error handling
//! - No string operations in critical paths
//! - Optional wee_alloc for smaller binary
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
// Use wee_alloc for smaller WASM binary (~10KB reduction)
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
/// Minimal DAG node - 9 bytes (u32 + u8 + f32)
#[derive(Serialize, Deserialize, Clone, Copy)]
struct WasmNode {
id: u32,
op: u8,
cost: f32,
}
/// Minimal DAG structure for WASM
/// Self-contained with no external dependencies beyond wasm-bindgen
#[wasm_bindgen]
pub struct WasmDag {
nodes: Vec<WasmNode>,
edges: Vec<(u32, u32)>,
}
#[wasm_bindgen]
impl WasmDag {
/// Create new empty DAG
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
nodes: Vec::new(),
edges: Vec::new(),
}
}
/// Add a node with operator type and cost
/// Returns node ID
#[inline]
pub fn add_node(&mut self, op: u8, cost: f32) -> u32 {
let id = self.nodes.len() as u32;
self.nodes.push(WasmNode { id, op, cost });
id
}
/// Add edge from -> to
/// Returns false if creates cycle (simple check)
#[inline]
pub fn add_edge(&mut self, from: u32, to: u32) -> bool {
// Basic validation - nodes must exist
if from >= self.nodes.len() as u32 || to >= self.nodes.len() as u32 {
return false;
}
// Simple cycle check: to must not reach from
if self.has_path(to, from) {
return false;
}
self.edges.push((from, to));
true
}
/// Get number of nodes
#[inline]
pub fn node_count(&self) -> u32 {
self.nodes.len() as u32
}
/// Get number of edges
#[inline]
pub fn edge_count(&self) -> u32 {
self.edges.len() as u32
}
/// Topological sort using Kahn's algorithm
/// Returns node IDs in topological order
pub fn topo_sort(&self) -> Vec<u32> {
let n = self.nodes.len();
let mut in_degree = vec![0u32; n];
// Calculate in-degrees
for &(_, to) in &self.edges {
in_degree[to as usize] += 1;
}
// Find nodes with no incoming edges
let mut queue: Vec<u32> = (0..n as u32)
.filter(|&i| in_degree[i as usize] == 0)
.collect();
let mut result = Vec::with_capacity(n);
while let Some(node) = queue.pop() {
result.push(node);
// Reduce in-degree for neighbors
for &(from, to) in &self.edges {
if from == node {
in_degree[to as usize] -= 1;
if in_degree[to as usize] == 0 {
queue.push(to);
}
}
}
}
result
}
/// Find critical path (longest path by cost)
/// Returns JSON: {"path": [node_ids], "cost": total}
pub fn critical_path(&self) -> JsValue {
let topo = self.topo_sort();
let n = self.nodes.len();
// dist[i] = (max_cost_to_i, predecessor)
let mut dist = vec![(0.0f32, u32::MAX); n];
// Initialize starting nodes
for &node in &topo {
if !self.has_incoming(node) {
dist[node as usize] = (self.nodes[node as usize].cost, u32::MAX);
}
}
// Relax edges in topological order
for &from in &topo {
let from_cost = dist[from as usize].0;
for &(f, to) in &self.edges {
if f == from {
let new_cost = from_cost + self.nodes[to as usize].cost;
if new_cost > dist[to as usize].0 {
dist[to as usize] = (new_cost, from);
}
}
}
}
// Find node with maximum cost
let (max_idx, (max_cost, _)) = dist
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.0.partial_cmp(&b.0).unwrap())
.unwrap();
// Backtrack to build path
let mut path = Vec::new();
let mut current = max_idx as u32;
while current != u32::MAX {
path.push(current);
current = dist[current as usize].1;
}
path.reverse();
// Convert to JSON manually to avoid serde_json dependency
let path_str = path
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
let json = format!("{{\"path\":[{}],\"cost\":{}}}", path_str, max_cost);
JsValue::from_str(&json)
}
/// Compute attention scores for nodes
/// mechanism: 0=topological, 1=critical_path, 2=uniform
pub fn attention(&self, mechanism: u8) -> Vec<f32> {
compute_attention(self, mechanism)
}
/// Serialize to bytes (bincode format)
pub fn to_bytes(&self) -> Vec<u8> {
#[derive(Serialize)]
struct SerDag<'a> {
nodes: &'a [WasmNode],
edges: &'a [(u32, u32)],
}
let data = SerDag {
nodes: &self.nodes,
edges: &self.edges,
};
bincode::serialize(&data).unwrap_or_default()
}
/// Deserialize from bytes
pub fn from_bytes(data: &[u8]) -> Result<WasmDag, JsValue> {
#[derive(Deserialize)]
struct SerDag {
nodes: Vec<WasmNode>,
edges: Vec<(u32, u32)>,
}
bincode::deserialize::<SerDag>(data)
.map(|d| WasmDag {
nodes: d.nodes,
edges: d.edges,
})
.map_err(|e| JsValue::from_str(&format!("Deserialize error: {}", e)))
}
/// Serialize to JSON
pub fn to_json(&self) -> String {
#[derive(Serialize)]
struct SerDag<'a> {
nodes: &'a [WasmNode],
edges: &'a [(u32, u32)],
}
let data = SerDag {
nodes: &self.nodes,
edges: &self.edges,
};
serde_json::to_string(&data).unwrap_or_else(|_| String::from("{}"))
}
/// Deserialize from JSON
pub fn from_json(json: &str) -> Result<WasmDag, JsValue> {
#[derive(Deserialize)]
struct SerDag {
nodes: Vec<WasmNode>,
edges: Vec<(u32, u32)>,
}
serde_json::from_str::<SerDag>(json)
.map(|d| WasmDag {
nodes: d.nodes,
edges: d.edges,
})
.map_err(|e| JsValue::from_str(&format!("JSON error: {}", e)))
}
}
// Internal helper methods (not exported to WASM)
impl WasmDag {
/// Check if there's a path from 'from' to 'to' (for cycle detection)
#[inline(always)]
fn has_path(&self, from: u32, to: u32) -> bool {
if from == to {
return true;
}
let mut visited = vec![false; self.nodes.len()];
let mut stack = Vec::with_capacity(8);
stack.push(from);
while let Some(node) = stack.pop() {
if visited[node as usize] {
continue;
}
visited[node as usize] = true;
for &(f, t) in &self.edges {
if f == node {
if t == to {
return true;
}
stack.push(t);
}
}
}
false
}
/// Check if node has incoming edges
#[inline(always)]
fn has_incoming(&self, node: u32) -> bool {
self.edges.iter().any(|&(_, to)| to == node)
}
}
/// Compute attention scores based on mechanism
///
/// Mechanisms:
/// - 0: Topological (position in topo sort)
/// - 1: Critical path (distance from critical path)
/// - 2: Uniform (all equal)
#[inline]
fn compute_attention(dag: &WasmDag, mechanism: u8) -> Vec<f32> {
let n = dag.nodes.len();
match mechanism {
0 => {
// Topological attention - earlier nodes get higher scores
let topo = dag.topo_sort();
let mut scores = vec![0.0f32; n];
for (i, &node_id) in topo.iter().enumerate() {
scores[node_id as usize] = 1.0 - (i as f32 / n as f32);
}
scores
}
1 => {
// Critical path attention - nodes on/near critical path get higher scores
let topo = dag.topo_sort();
let mut dist = vec![0.0f32; n];
// Forward pass - compute longest path to each node
for &from in &topo {
for &(f, to) in &dag.edges {
if f == from {
let new_dist = dist[from as usize] + dag.nodes[to as usize].cost;
if new_dist > dist[to as usize] {
dist[to as usize] = new_dist;
}
}
}
}
// Normalize to [0, 1]
let max_dist = dist.iter().fold(0.0f32, |a, &b| a.max(b));
if max_dist > 0.0 {
dist.iter_mut().for_each(|d| *d /= max_dist);
}
dist
}
_ => {
// Uniform attention
vec![1.0f32 / n as f32; n]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_dag() {
let mut dag = WasmDag::new();
let n0 = dag.add_node(1, 1.0);
let n1 = dag.add_node(2, 2.0);
let n2 = dag.add_node(3, 3.0);
assert_eq!(dag.node_count(), 3);
assert!(dag.add_edge(n0, n1));
assert!(dag.add_edge(n1, n2));
assert_eq!(dag.edge_count(), 2);
// Should detect cycle
assert!(!dag.add_edge(n2, n0));
}
#[test]
fn test_topo_sort() {
let mut dag = WasmDag::new();
let n0 = dag.add_node(0, 1.0);
let n1 = dag.add_node(1, 1.0);
let n2 = dag.add_node(2, 1.0);
dag.add_edge(n0, n1);
dag.add_edge(n1, n2);
let topo = dag.topo_sort();
assert_eq!(topo, vec![0, 1, 2]);
}
#[test]
fn test_attention() {
let mut dag = WasmDag::new();
dag.add_node(0, 1.0);
dag.add_node(1, 2.0);
dag.add_node(2, 3.0);
// Uniform
let uniform = dag.attention(2);
assert_eq!(uniform.len(), 3);
assert!((uniform[0] - 0.333).abs() < 0.01);
// Topological
let topo = dag.attention(0);
assert_eq!(topo.len(), 3);
}
#[test]
fn test_serialization() {
let mut dag = WasmDag::new();
dag.add_node(1, 1.5);
dag.add_node(2, 2.5);
dag.add_edge(0, 1);
// Binary
let bytes = dag.to_bytes();
let restored = WasmDag::from_bytes(&bytes).unwrap();
assert_eq!(restored.node_count(), 2);
assert_eq!(restored.edge_count(), 1);
// JSON
let json = dag.to_json();
let from_json = WasmDag::from_json(&json).unwrap();
assert_eq!(from_json.node_count(), 2);
}
}