396 lines
14 KiB
Rust
396 lines
14 KiB
Rust
//! RVF Self-Learning Solver WASM Module
|
|
//!
|
|
//! Exposes the complete AGI temporal reasoning engine as WASM exports:
|
|
//! - PolicyKernel with Thompson Sampling (two-signal model)
|
|
//! - Context-bucketed bandit (18 buckets: 3 range x 3 distractor x 2 noise)
|
|
//! - KnowledgeCompiler with signature-based pattern cache
|
|
//! - Speculative dual-path execution
|
|
//! - Three-loop adaptive solver (fast/medium/slow)
|
|
//! - Acceptance test with training/holdout cycles
|
|
//! - SHAKE-256 witness chain via rvf-crypto
|
|
//!
|
|
//! Target: wasm32-unknown-unknown, no_std + alloc.
|
|
//!
|
|
//! ## WASM Exports
|
|
//!
|
|
//! | Export | Description |
|
|
//! |--------|-------------|
|
|
//! | `rvf_solver_alloc` | Allocate WASM memory |
|
|
//! | `rvf_solver_free` | Free WASM memory |
|
|
//! | `rvf_solver_create` | Create solver instance → handle |
|
|
//! | `rvf_solver_destroy` | Destroy solver instance |
|
|
//! | `rvf_solver_train` | Train on generated puzzles |
|
|
//! | `rvf_solver_acceptance` | Run full acceptance test |
|
|
//! | `rvf_solver_result_len` | Get last result JSON length |
|
|
//! | `rvf_solver_result_read` | Read last result JSON |
|
|
//! | `rvf_solver_policy_len` | Get policy state JSON length |
|
|
//! | `rvf_solver_policy_read` | Read policy state JSON |
|
|
//! | `rvf_solver_witness_len` | Get witness chain byte length |
|
|
//! | `rvf_solver_witness_read` | Read witness chain bytes |
|
|
|
|
#![no_std]
|
|
|
|
extern crate alloc;
|
|
|
|
mod alloc_setup;
|
|
pub mod engine;
|
|
pub mod policy;
|
|
pub mod types;
|
|
|
|
use alloc::vec::Vec;
|
|
|
|
use engine::{AcceptanceConfig, AcceptanceResult, AdaptiveSolver, PuzzleGenerator, run_acceptance_mode};
|
|
use rvf_crypto::{create_witness_chain, WitnessEntry};
|
|
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
// Instance registry (max 8 concurrent solvers)
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
|
|
const MAX_INSTANCES: usize = 8;
|
|
|
|
struct SolverInstance {
|
|
solver: AdaptiveSolver,
|
|
last_result_json: Vec<u8>,
|
|
policy_json: Vec<u8>,
|
|
witness_chain: Vec<u8>,
|
|
}
|
|
|
|
struct Registry {
|
|
slots: [Option<SolverInstance>; MAX_INSTANCES],
|
|
}
|
|
|
|
impl Registry {
|
|
const fn new() -> Self {
|
|
Self {
|
|
slots: [const { None }; MAX_INSTANCES],
|
|
}
|
|
}
|
|
|
|
fn create(&mut self) -> i32 {
|
|
for (i, slot) in self.slots.iter_mut().enumerate() {
|
|
if slot.is_none() {
|
|
*slot = Some(SolverInstance {
|
|
solver: AdaptiveSolver::new(),
|
|
last_result_json: Vec::new(),
|
|
policy_json: Vec::new(),
|
|
witness_chain: Vec::new(),
|
|
});
|
|
return (i + 1) as i32;
|
|
}
|
|
}
|
|
-1
|
|
}
|
|
|
|
fn get(&self, handle: i32) -> Option<&SolverInstance> {
|
|
let idx = (handle - 1) as usize;
|
|
if idx < MAX_INSTANCES {
|
|
self.slots[idx].as_ref()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_mut(&mut self, handle: i32) -> Option<&mut SolverInstance> {
|
|
let idx = (handle - 1) as usize;
|
|
if idx < MAX_INSTANCES {
|
|
self.slots[idx].as_mut()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn destroy(&mut self, handle: i32) -> i32 {
|
|
let idx = (handle - 1) as usize;
|
|
if idx < MAX_INSTANCES && self.slots[idx].is_some() {
|
|
self.slots[idx] = None;
|
|
0
|
|
} else {
|
|
-1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Global mutable registry — safe in single-threaded WASM.
|
|
static mut REGISTRY: Registry = Registry::new();
|
|
|
|
#[allow(static_mut_refs)]
|
|
fn registry() -> &'static mut Registry {
|
|
unsafe { &mut REGISTRY }
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
// WASM Exports — Lifecycle
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
|
|
/// Create a new solver instance. Returns handle (>0) or -1 on error.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_create() -> i32 {
|
|
registry().create()
|
|
}
|
|
|
|
/// Destroy a solver instance. Returns 0 on success.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_destroy(handle: i32) -> i32 {
|
|
registry().destroy(handle)
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
// WASM Exports — Training
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
|
|
/// Train the solver on `count` generated puzzles.
|
|
///
|
|
/// Uses the three-loop architecture: fast (solve), medium (PolicyKernel),
|
|
/// slow (KnowledgeCompiler). Returns number of puzzles solved correctly.
|
|
///
|
|
/// Parameters:
|
|
/// - handle: solver instance
|
|
/// - count: number of puzzles to generate and solve
|
|
/// - min_diff: minimum puzzle difficulty (1-10)
|
|
/// - max_diff: maximum puzzle difficulty (1-10)
|
|
/// - seed_lo: lower 32 bits of RNG seed
|
|
/// - seed_hi: upper 32 bits of RNG seed
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_train(
|
|
handle: i32,
|
|
count: i32,
|
|
min_diff: i32,
|
|
max_diff: i32,
|
|
seed_lo: i32,
|
|
seed_hi: i32,
|
|
) -> i32 {
|
|
let inst = match registry().get_mut(handle) {
|
|
Some(i) => i,
|
|
None => return -1,
|
|
};
|
|
|
|
let seed = ((seed_hi as u64) << 32) | (seed_lo as u64 & 0xFFFFFFFF);
|
|
let mut gen = PuzzleGenerator::new(seed, min_diff as u8, max_diff as u8);
|
|
let puzzles = gen.generate_batch(count as usize);
|
|
|
|
inst.solver.compiler_enabled = true;
|
|
inst.solver.router_enabled = true;
|
|
|
|
let mut correct = 0i32;
|
|
for puzzle in &puzzles {
|
|
let result = inst.solver.solve(puzzle);
|
|
if result.correct {
|
|
correct += 1;
|
|
}
|
|
}
|
|
|
|
// Promote learned patterns
|
|
inst.solver.bank.promote();
|
|
inst.solver.bank.compile_to(&mut inst.solver.compiler);
|
|
|
|
// Serialize result
|
|
let result_json = serde_json::to_vec(&AcceptanceSummary {
|
|
trained: count as usize,
|
|
correct: correct as usize,
|
|
accuracy: correct as f64 / count as f64,
|
|
patterns_learned: inst.solver.bank.patterns_learned,
|
|
})
|
|
.unwrap_or_default();
|
|
inst.last_result_json = result_json;
|
|
|
|
correct
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct AcceptanceSummary {
|
|
trained: usize,
|
|
correct: usize,
|
|
accuracy: f64,
|
|
patterns_learned: usize,
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
// WASM Exports — Acceptance Test
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
|
|
/// Run the full acceptance test with training/holdout cycles.
|
|
///
|
|
/// Runs all three ablation modes (A/B/C) and produces a manifest.
|
|
/// Returns: 1 = passed, 0 = failed, -1 = error.
|
|
///
|
|
/// After this call, use `rvf_solver_result_len` / `rvf_solver_result_read`
|
|
/// to retrieve the full manifest JSON.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_acceptance(
|
|
handle: i32,
|
|
holdout: i32,
|
|
training: i32,
|
|
cycles: i32,
|
|
budget: i32,
|
|
seed_lo: i32,
|
|
seed_hi: i32,
|
|
) -> i32 {
|
|
let inst = match registry().get_mut(handle) {
|
|
Some(i) => i,
|
|
None => return -1,
|
|
};
|
|
|
|
let seed = ((seed_hi as u64) << 32) | (seed_lo as u64 & 0xFFFFFFFF);
|
|
let config = AcceptanceConfig {
|
|
holdout_size: holdout as usize,
|
|
training_per_cycle: training as usize,
|
|
cycles: cycles as usize,
|
|
step_budget: budget as usize,
|
|
holdout_seed: seed,
|
|
training_seed: seed.wrapping_add(1),
|
|
noise_rate: 0.25,
|
|
min_accuracy: 0.80,
|
|
};
|
|
|
|
// Run all three modes
|
|
let mode_a = run_acceptance_mode(&config, false, false);
|
|
let mode_b = run_acceptance_mode(&config, true, false);
|
|
let mode_c = run_acceptance_mode(&config, true, true);
|
|
|
|
// Build witness chain from cycle metrics
|
|
let mut witness_entries: Vec<WitnessEntry> = Vec::new();
|
|
let mut seq: u64 = 0;
|
|
for (label, result) in [("A", &mode_a), ("B", &mode_b), ("C", &mode_c)] {
|
|
for cm in &result.cycles {
|
|
let mut action_data = Vec::with_capacity(64);
|
|
action_data.extend_from_slice(label.as_bytes());
|
|
action_data.extend_from_slice(&(cm.cycle as u64).to_le_bytes());
|
|
action_data.extend_from_slice(&cm.accuracy.to_le_bytes());
|
|
action_data.extend_from_slice(&cm.cost_per_solve.to_le_bytes());
|
|
let action_hash = rvf_crypto::shake256_256(&action_data);
|
|
witness_entries.push(WitnessEntry {
|
|
prev_hash: [0u8; 32],
|
|
action_hash,
|
|
timestamp_ns: seq,
|
|
witness_type: 0x02,
|
|
});
|
|
seq += 1;
|
|
}
|
|
}
|
|
|
|
// Create SHAKE-256 witness chain
|
|
inst.witness_chain = create_witness_chain(&witness_entries);
|
|
|
|
// Build manifest JSON
|
|
let manifest = AcceptanceManifest {
|
|
version: 2,
|
|
mode_a,
|
|
mode_b,
|
|
mode_c: mode_c.clone(),
|
|
all_passed: mode_c.passed, // C is the full mode
|
|
witness_entries: witness_entries.len(),
|
|
witness_chain_bytes: inst.witness_chain.len(),
|
|
};
|
|
|
|
inst.last_result_json = serde_json::to_vec(&manifest).unwrap_or_default();
|
|
|
|
// Update solver state with Mode C results
|
|
inst.solver.compiler_enabled = true;
|
|
inst.solver.router_enabled = true;
|
|
|
|
// Serialize policy state
|
|
inst.policy_json = serde_json::to_vec(&inst.solver.policy_kernel).unwrap_or_default();
|
|
|
|
if mode_c.passed { 1 } else { 0 }
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct AcceptanceManifest {
|
|
version: u32,
|
|
mode_a: AcceptanceResult,
|
|
mode_b: AcceptanceResult,
|
|
mode_c: AcceptanceResult,
|
|
all_passed: bool,
|
|
witness_entries: usize,
|
|
witness_chain_bytes: usize,
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
// WASM Exports — Result / Policy / Witness reads
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
|
|
/// Get the byte length of the last result JSON.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_result_len(handle: i32) -> i32 {
|
|
registry()
|
|
.get(handle)
|
|
.map(|i| i.last_result_json.len() as i32)
|
|
.unwrap_or(-1)
|
|
}
|
|
|
|
/// Copy the last result JSON into `out_ptr`. Returns bytes written.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_result_read(handle: i32, out_ptr: i32) -> i32 {
|
|
let inst = match registry().get(handle) {
|
|
Some(i) => i,
|
|
None => return -1,
|
|
};
|
|
let data = &inst.last_result_json;
|
|
unsafe {
|
|
core::ptr::copy_nonoverlapping(data.as_ptr(), out_ptr as *mut u8, data.len());
|
|
}
|
|
data.len() as i32
|
|
}
|
|
|
|
/// Get the byte length of the policy state JSON.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_policy_len(handle: i32) -> i32 {
|
|
let inst = match registry().get_mut(handle) {
|
|
Some(i) => i,
|
|
None => return -1,
|
|
};
|
|
// Refresh policy JSON
|
|
inst.policy_json = serde_json::to_vec(&inst.solver.policy_kernel).unwrap_or_default();
|
|
inst.policy_json.len() as i32
|
|
}
|
|
|
|
/// Copy the policy state JSON into `out_ptr`. Returns bytes written.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_policy_read(handle: i32, out_ptr: i32) -> i32 {
|
|
let inst = match registry().get(handle) {
|
|
Some(i) => i,
|
|
None => return -1,
|
|
};
|
|
let data = &inst.policy_json;
|
|
unsafe {
|
|
core::ptr::copy_nonoverlapping(data.as_ptr(), out_ptr as *mut u8, data.len());
|
|
}
|
|
data.len() as i32
|
|
}
|
|
|
|
/// Get the byte length of the witness chain (73 bytes per entry).
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_witness_len(handle: i32) -> i32 {
|
|
registry()
|
|
.get(handle)
|
|
.map(|i| i.witness_chain.len() as i32)
|
|
.unwrap_or(-1)
|
|
}
|
|
|
|
/// Copy the raw witness chain bytes into `out_ptr`.
|
|
///
|
|
/// The witness chain is in native rvf-crypto format: 73 bytes per entry,
|
|
/// verifiable by `rvf_witness_verify` in the rvf-wasm microkernel.
|
|
#[no_mangle]
|
|
pub extern "C" fn rvf_solver_witness_read(handle: i32, out_ptr: i32) -> i32 {
|
|
let inst = match registry().get(handle) {
|
|
Some(i) => i,
|
|
None => return -1,
|
|
};
|
|
let data = &inst.witness_chain;
|
|
unsafe {
|
|
core::ptr::copy_nonoverlapping(data.as_ptr(), out_ptr as *mut u8, data.len());
|
|
}
|
|
data.len() as i32
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
// Panic handler
|
|
// ═════════════════════════════════════════════════════════════════════
|
|
|
|
#[cfg(not(test))]
|
|
#[panic_handler]
|
|
fn panic(_info: &core::panic::PanicInfo) -> ! {
|
|
core::arch::wasm32::unreachable()
|
|
}
|