use memmap2::MmapMut;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::hash::Hasher;
pub mod sampling {
use std::cell::Cell;
use std::sync::atomic::{AtomicU64, Ordering};
static GLOBAL_TRACE_COUNT: AtomicU64 = AtomicU64::new(0);
pub const GLOBAL_TRACE_LIMIT: u64 = 10_000;
thread_local! {
static RNG_STATE: Cell<u64> = Cell::new(seed_from_thread_id());
}
fn seed_from_thread_id() -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let thread_id = std::thread::current().id();
let mut hasher = DefaultHasher::new();
thread_id.hash(&mut hasher);
let seed = hasher.finish();
if seed == 0 {
0xcafebabe
} else {
seed
}
}
#[inline(always)]
pub fn fast_random() -> u64 {
RNG_STATE.with(|state| {
let mut x = state.get();
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
state.set(x);
x
})
}
#[inline(always)]
pub fn should_sample_trace(probability: f64) -> bool {
contract_pre_error_handling!();
let current_count = GLOBAL_TRACE_COUNT.load(Ordering::Relaxed);
if current_count >= GLOBAL_TRACE_LIMIT {
return false; }
let threshold = (probability * u64::MAX as f64) as u64;
if fast_random() < threshold {
GLOBAL_TRACE_COUNT.fetch_add(1, Ordering::Relaxed);
return true;
}
false
}
pub fn reset_trace_counter() {
contract_pre_error_handling!();
GLOBAL_TRACE_COUNT.store(0, Ordering::Relaxed);
}
pub fn get_trace_count() -> u64 {
GLOBAL_TRACE_COUNT.load(Ordering::Relaxed)
}
pub fn set_trace_limit(_limit: u64) {
GLOBAL_TRACE_COUNT.store(0, Ordering::Relaxed);
}
}
pub mod categories {
pub const TYPE_INFERENCE: &str = "type_inference";
pub const TYPE_INFERENCE_FUNCTION: &str = "type_inference::infer_function";
pub const TYPE_INFERENCE_VARIABLE: &str = "type_inference::infer_variable";
pub const TYPE_INFERENCE_COERCE: &str = "type_inference::coerce_type";
pub const TYPE_INFERENCE_GENERIC: &str = "type_inference::generic_instantiation";
pub const OPTIMIZATION: &str = "optimization";
pub const OPTIMIZATION_INLINE: &str = "optimization::inline_candidate";
pub const OPTIMIZATION_ESCAPE: &str = "optimization::escape_analysis";
pub const OPTIMIZATION_TAIL_RECURSION: &str = "optimization::tail_recursion";
pub const OPTIMIZATION_CONST_FOLDING: &str = "optimization::constant_folding";
pub const OPTIMIZATION_DEAD_CODE: &str = "optimization::dead_code_elimination";
pub const CODEGEN: &str = "codegen";
pub const CODEGEN_INTEGER_TYPE: &str = "codegen::integer_type";
pub const CODEGEN_STRING_STRATEGY: &str = "codegen::string_strategy";
pub const CODEGEN_COLLECTION_TYPE: &str = "codegen::collection_type";
pub const CODEGEN_ERROR_HANDLING: &str = "codegen::error_handling";
pub const STDLIB: &str = "stdlib";
pub const STDLIB_IO_MAPPING: &str = "stdlib::io_mapping";
pub const STDLIB_STRING_METHOD: &str = "stdlib::string_method";
pub const STDLIB_ARRAY_METHOD: &str = "stdlib::array_method";
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecisionTrace {
pub timestamp_us: u64,
pub category: String,
pub name: String,
pub input: serde_json::Value,
pub result: Option<serde_json::Value>,
pub source_location: Option<String>,
pub decision_id: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecisionManifestEntry {
pub decision_id: u64,
pub category: String,
pub name: String,
pub source: SourceLocation,
pub input: serde_json::Value,
pub result: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SourceLocation {
pub file: String,
pub line: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecisionManifest {
pub version: String,
pub git_commit: Option<String>,
pub transpiler_version: Option<String>,
#[serde(flatten)]
pub entries: HashMap<String, DecisionManifestEntry>,
}
pub fn generate_decision_id(category: &str, name: &str, file: &str, line: u32) -> u64 {
let mut hasher = fnv::FnvHasher::default();
hasher.write(category.as_bytes());
hasher.write(b"::");
hasher.write(name.as_bytes());
hasher.write(b"::");
hasher.write(file.as_bytes());
hasher.write(b"::");
hasher.write(&line.to_le_bytes());
hasher.finish()
}
impl DecisionManifest {
pub fn load_from_file(path: &std::path::Path) -> Result<Self, String> {
let contents = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read manifest file: {e}"))?;
let manifest: DecisionManifest = serde_json::from_str(&contents)
.map_err(|e| format!("Failed to parse manifest JSON: {e}"))?;
Ok(manifest)
}
}
pub fn read_decisions_from_msgpack(path: &std::path::Path) -> Result<Vec<DecisionTrace>, String> {
let contents = std::fs::read(path).map_err(|e| format!("Failed to read msgpack file: {e}"))?;
if contents.is_empty() {
return Err("MessagePack file is empty".to_string());
}
let traces: Vec<DecisionTrace> = rmp_serde::from_slice(&contents)
.map_err(|e| format!("Failed to deserialize msgpack: {e}"))?;
Ok(traces)
}
pub struct MmapDecisionWriter {
mmap: MmapMut,
offset: usize,
decisions: Vec<DecisionTrace>,
}
impl MmapDecisionWriter {
#[allow(unsafe_code)]
pub fn new(path: &std::path::Path, size: usize) -> Result<Self, String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create parent directory: {e}"))?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|e| format!("Failed to create file: {e}"))?;
file.set_len(size as u64).map_err(|e| format!("Failed to set file size: {e}"))?;
let mmap = unsafe {
MmapMut::map_mut(&file).map_err(|e| format!("Failed to create memory map: {e}"))?
};
Ok(Self { mmap, offset: 0, decisions: Vec::new() })
}
pub fn append(&mut self, decision: &DecisionTrace) -> Result<(), String> {
self.decisions.push(decision.clone());
Ok(())
}
pub fn flush(&mut self) -> Result<(), String> {
if self.decisions.is_empty() {
return Ok(());
}
let packed = rmp_serde::to_vec(&self.decisions)
.map_err(|e| format!("Failed to serialize decisions: {e}"))?;
if self.offset + packed.len() > self.mmap.len() {
return Err(format!(
"Memory-mapped file too small: need {} bytes, have {} bytes remaining",
packed.len(),
self.mmap.len() - self.offset
));
}
self.mmap[self.offset..self.offset + packed.len()].copy_from_slice(&packed);
self.offset += packed.len();
self.mmap.flush().map_err(|e| format!("Failed to flush mmap: {e}"))?;
Ok(())
}
pub fn len(&self) -> usize {
self.decisions.len()
}
pub fn is_empty(&self) -> bool {
self.decisions.is_empty()
}
}
impl Drop for MmapDecisionWriter {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[derive(Debug)]
pub struct DecisionTracer {
traces: Vec<DecisionTrace>,
start_time: std::time::Instant,
}
impl DecisionTracer {
pub fn new() -> Self {
Self { traces: Vec::new(), start_time: std::time::Instant::now() }
}
pub fn parse_line(&mut self, line: &str) -> Result<(), String> {
let timestamp_us = self.start_time.elapsed().as_micros() as u64;
if line.starts_with("[DECISION]") {
self.parse_decision_line(line, timestamp_us)
} else if line.starts_with("[RESULT]") {
self.parse_result_line(line, timestamp_us)
} else {
Ok(())
}
}
fn parse_decision_line(&mut self, line: &str, timestamp_us: u64) -> Result<(), String> {
let content = line.strip_prefix("[DECISION]").ok_or("Missing [DECISION] prefix")?.trim();
let parts: Vec<&str> = content.splitn(2, " input=").collect();
if parts.len() != 2 {
return Err(format!("Invalid DECISION format: {line}"));
}
let category_name = parts[0];
let cat_name_parts: Vec<&str> = category_name.split("::").collect();
if cat_name_parts.len() != 2 {
return Err(format!("Invalid category::name format: {category_name}"));
}
let category = cat_name_parts[0].to_string();
let name = cat_name_parts[1].to_string();
let input: serde_json::Value =
serde_json::from_str(parts[1]).map_err(|e| format!("Invalid JSON input: {e}"))?;
self.traces.push(DecisionTrace {
timestamp_us,
category,
name,
input,
result: None,
source_location: None,
decision_id: None, });
Ok(())
}
fn parse_result_line(&mut self, line: &str, _timestamp_us: u64) -> Result<(), String> {
let content = line.strip_prefix("[RESULT]").ok_or("Missing [RESULT] prefix")?.trim();
let parts: Vec<&str> = content.splitn(2, " = ").collect();
if parts.len() != 2 {
return Err(format!("Invalid RESULT format: {line}"));
}
let name = parts[0].trim();
let result: serde_json::Value =
serde_json::from_str(parts[1]).map_err(|e| format!("Invalid JSON result: {e}"))?;
for trace in self.traces.iter_mut().rev() {
if trace.name == name && trace.result.is_none() {
trace.result = Some(result);
return Ok(());
}
}
Err(format!("No matching DECISION found for RESULT: {name}"))
}
pub fn traces(&self) -> &[DecisionTrace] {
&self.traces
}
pub fn count(&self) -> usize {
self.traces.len()
}
pub fn add_decision_with_id(
&mut self,
category: &str,
name: &str,
input: serde_json::Value,
result: Option<serde_json::Value>,
source_location: Option<&str>,
decision_id: Option<u64>,
) {
let timestamp_us = self.start_time.elapsed().as_micros() as u64;
self.traces.push(DecisionTrace {
timestamp_us,
category: category.to_string(),
name: name.to_string(),
input,
result,
source_location: source_location.map(std::string::ToString::to_string),
decision_id,
});
}
pub fn write_to_msgpack(&self, path: &std::path::Path) -> Result<(), String> {
let packed = rmp_serde::to_vec(&self.traces)
.map_err(|e| format!("Failed to serialize traces to MessagePack: {e}"))?;
std::fs::write(path, packed)
.map_err(|e| format!("Failed to write MessagePack file: {e}"))?;
Ok(())
}
pub fn write_manifest(
&self,
path: &std::path::Path,
version: &str,
git_commit: Option<&str>,
transpiler_version: Option<&str>,
) -> Result<(), String> {
let mut entries = HashMap::new();
for trace in &self.traces {
if let Some(decision_id) = trace.decision_id {
let source = if let Some(ref loc) = trace.source_location {
let parts: Vec<&str> = loc.split(':').collect();
if parts.len() >= 2 {
let file = parts[0].to_string();
let line = parts[1].parse::<u32>().unwrap_or(0);
let column =
if parts.len() >= 3 { parts[2].parse::<u32>().ok() } else { None };
SourceLocation { file, line, column }
} else {
SourceLocation { file: loc.clone(), line: 0, column: None }
}
} else {
SourceLocation { file: "unknown".to_string(), line: 0, column: None }
};
let entry = DecisionManifestEntry {
decision_id,
category: trace.category.clone(),
name: trace.name.clone(),
source,
input: trace.input.clone(),
result: trace.result.clone().unwrap_or(serde_json::Value::Null),
};
let key = format!("0x{decision_id:X}");
entries.insert(key, entry);
}
}
let manifest = DecisionManifest {
version: version.to_string(),
git_commit: git_commit.map(std::string::ToString::to_string),
transpiler_version: transpiler_version.map(std::string::ToString::to_string),
entries,
};
let json = serde_json::to_string_pretty(&manifest)
.map_err(|e| format!("Failed to serialize manifest to JSON: {e}"))?;
std::fs::write(path, json).map_err(|e| format!("Failed to write manifest file: {e}"))?;
Ok(())
}
}
impl Default for DecisionTracer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct DecisionGraph {
dependencies: HashMap<usize, Vec<usize>>,
traces: Vec<DecisionTrace>,
}
impl DecisionGraph {
pub fn from_traces(traces: Vec<DecisionTrace>) -> Self {
let mut graph = Self { dependencies: HashMap::new(), traces };
graph.analyze_dependencies();
graph
}
fn analyze_dependencies(&mut self) {
for i in 0..self.traces.len() {
for j in (i + 1)..self.traces.len() {
if self.has_dependency(i, j) {
self.dependencies.entry(i).or_default().push(j);
}
}
}
}
fn has_dependency(&self, i: usize, j: usize) -> bool {
if let Some(ref result_i) = self.traces[i].result {
let input_j = &self.traces[j].input;
let result_str = serde_json::to_string(result_i).unwrap_or_default();
let input_str = serde_json::to_string(input_j).unwrap_or_default();
if result_str.len() >= 3 && input_str.contains(&result_str[1..result_str.len() - 1]) {
return true;
}
}
false
}
pub fn find_cascades(&self) -> Vec<Vec<usize>> {
let mut cascades = Vec::new();
for start_idx in 0..self.traces.len() {
let mut cascade = vec![start_idx];
let mut current = start_idx;
while let Some(deps) = self.dependencies.get(¤t) {
if let Some(&next) = deps.first() {
cascade.push(next);
current = next;
} else {
break;
}
}
if cascade.len() > 1 {
cascades.push(cascade);
}
}
cascades
}
pub fn dependencies(&self, decision_idx: usize) -> Option<&Vec<usize>> {
self.dependencies.get(&decision_idx)
}
}
static_assertions::assert_impl_all!(DecisionTrace: Send, Sync);
static_assertions::assert_impl_all!(DecisionManifestEntry: Send, Sync);
static_assertions::assert_impl_all!(DecisionManifest: Send, Sync);
static_assertions::assert_impl_all!(DecisionGraph: Send, Sync);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decision_tracer_new() {
let tracer = DecisionTracer::new();
assert_eq!(tracer.count(), 0);
}
#[test]
fn test_parse_decision_line_basic() {
let mut tracer = DecisionTracer::new();
let result =
tracer.parse_line(r#"[DECISION] exception_flow::try_body input={"handlers":2}"#);
assert!(result.is_ok(), "Parse should succeed: {:?}", result);
assert_eq!(tracer.count(), 1);
let trace = &tracer.traces()[0];
assert_eq!(trace.category, "exception_flow");
assert_eq!(trace.name, "try_body");
assert_eq!(trace.input["handlers"], 2);
}
#[test]
fn test_parse_result_line() {
let mut tracer = DecisionTracer::new();
tracer
.parse_line(r#"[DECISION] type_inference::infer_return input={"func":"foo"}"#)
.expect("test");
tracer.parse_line(r#"[RESULT] infer_return = {"type":"i32"}"#).expect("test");
assert_eq!(tracer.count(), 1);
let trace = &tracer.traces()[0];
assert!(trace.result.is_some());
assert_eq!(trace.result.as_ref().expect("test")["type"], "i32");
}
#[test]
fn test_parse_invalid_decision_format() {
let mut tracer = DecisionTracer::new();
let result = tracer.parse_line("[DECISION] invalid");
assert!(result.is_err());
}
#[test]
fn test_parse_invalid_json() {
let mut tracer = DecisionTracer::new();
let result = tracer.parse_line(r"[DECISION] cat::name input={invalid}");
assert!(result.is_err());
}
#[test]
fn test_ignore_non_decision_lines() {
let mut tracer = DecisionTracer::new();
let result = tracer.parse_line("Normal stderr output");
assert!(result.is_ok());
assert_eq!(tracer.count(), 0);
}
#[test]
fn test_decision_graph_empty() {
let graph = DecisionGraph::from_traces(vec![]);
let cascades = graph.find_cascades();
assert!(cascades.is_empty());
}
#[test]
fn test_decision_graph_single_decision() {
let trace = DecisionTrace {
timestamp_us: 100,
category: "test".to_string(),
name: "decision1".to_string(),
input: serde_json::json!({"x": 1}),
result: Some(serde_json::json!({"y": 2})),
source_location: None,
decision_id: None,
};
let graph = DecisionGraph::from_traces(vec![trace]);
let cascades = graph.find_cascades();
assert!(cascades.is_empty()); }
mod sprint27_v2_tests {
use super::*;
#[test]
fn test_generate_decision_id_basic() {
let decision_id = generate_decision_id("optimization", "inline_candidate", "foo.rb", 3);
assert_ne!(decision_id, 0);
let decision_id2 =
generate_decision_id("optimization", "inline_candidate", "foo.rb", 3);
assert_eq!(decision_id, decision_id2);
}
#[test]
fn test_generate_decision_id_different_inputs_different_outputs() {
let id1 = generate_decision_id("optimization", "inline_candidate", "foo.rb", 3);
let id2 = generate_decision_id("type_inference", "inline_candidate", "foo.rb", 3);
assert_ne!(id1, id2);
let id3 = generate_decision_id("optimization", "inline_candidate", "foo.rb", 3);
let id4 = generate_decision_id("optimization", "tail_recursion", "foo.rb", 3);
assert_ne!(id3, id4);
let id5 = generate_decision_id("optimization", "inline_candidate", "foo.rb", 3);
let id6 = generate_decision_id("optimization", "inline_candidate", "bar.rb", 3);
assert_ne!(id5, id6);
let id7 = generate_decision_id("optimization", "inline_candidate", "foo.rb", 3);
let id8 = generate_decision_id("optimization", "inline_candidate", "foo.rb", 5);
assert_ne!(id7, id8);
}
#[test]
fn test_generate_decision_id_spec_example() {
let decision_id = generate_decision_id("optimization", "inline_candidate", "foo.rb", 3);
assert!(decision_id > 0);
}
#[test]
fn test_decision_manifest_entry_serialization() {
let entry = DecisionManifestEntry {
decision_id: 0xA1B2C3D4E5F67890,
category: "optimization".to_string(),
name: "inline_candidate".to_string(),
source: SourceLocation { file: "foo.rb".to_string(), line: 3, column: Some(1) },
input: serde_json::json!({"size": 4, "call_count": 1000}),
result: serde_json::json!({"decision": "no_inline", "reason": "recursive"}),
};
let json = serde_json::to_string(&entry).expect("test");
assert!(json.contains("decision_id"));
assert!(json.contains("optimization"));
assert!(json.contains("inline_candidate"));
assert!(json.contains("foo.rb"));
let deserialized: DecisionManifestEntry = serde_json::from_str(&json).expect("test");
assert_eq!(deserialized.decision_id, 0xA1B2C3D4E5F67890);
assert_eq!(deserialized.category, "optimization");
}
#[test]
fn test_decision_manifest_deserialization() {
let json = r#"{
"version": "2.0.0",
"git_commit": "abc123def",
"transpiler_version": "3.213.0",
"0xA1B2C3D4E5F67890": {
"decision_id": 11638049751140409488,
"category": "optimization",
"name": "inline_candidate",
"source": {
"file": "foo.rb",
"line": 3
},
"input": {"size": 4},
"result": {"decision": "no_inline"}
}
}"#;
let manifest: DecisionManifest = serde_json::from_str(json).expect("test");
assert_eq!(manifest.version, "2.0.0");
assert_eq!(manifest.git_commit, Some("abc123def".to_string()));
assert_eq!(manifest.entries.len(), 1);
}
#[test]
fn test_source_location_serialization() {
let loc = SourceLocation { file: "foo.rb".to_string(), line: 42, column: Some(10) };
let json = serde_json::to_string(&loc).expect("test");
assert!(json.contains("foo.rb"));
assert!(json.contains("42"));
assert!(json.contains("10"));
let loc2 = SourceLocation { file: "bar.rb".to_string(), line: 100, column: None };
let json2 = serde_json::to_string(&loc2).expect("test");
assert!(json2.contains("bar.rb"));
assert!(json2.contains("100"));
assert!(!json2.contains("column")); }
#[test]
fn test_load_decision_manifest_from_json() {
use std::io::Write;
use tempfile::NamedTempFile;
let manifest_json = r#"{
"version": "2.0.0",
"git_commit": "abc123",
"transpiler_version": "3.213.0",
"0xDEADBEEF": {
"decision_id": 3735928559,
"category": "optimization",
"name": "test_decision",
"source": {
"file": "test.rb",
"line": 1
},
"input": {"param": "value"},
"result": {"outcome": "success"}
}
}"#;
let mut temp_file = NamedTempFile::new().expect("test");
temp_file.write_all(manifest_json.as_bytes()).expect("test");
temp_file.flush().expect("test");
let manifest = DecisionManifest::load_from_file(temp_file.path()).expect("test");
assert_eq!(manifest.version, "2.0.0");
assert_eq!(manifest.git_commit, Some("abc123".to_string()));
assert_eq!(manifest.entries.len(), 1);
}
#[test]
fn test_load_decision_manifest_missing_file() {
let result =
DecisionManifest::load_from_file(std::path::Path::new("/nonexistent/path"));
assert!(result.is_err());
}
#[test]
fn test_load_decision_manifest_invalid_json() {
use std::io::Write;
use tempfile::NamedTempFile;
let mut temp_file = NamedTempFile::new().expect("test");
temp_file.write_all(b"not valid json {{{").expect("test");
temp_file.flush().expect("test");
let result = DecisionManifest::load_from_file(temp_file.path());
assert!(result.is_err());
}
#[test]
fn test_messagepack_decision_trace_roundtrip() {
let trace = DecisionTrace {
timestamp_us: 12345,
category: "optimization".to_string(),
name: "inline_candidate".to_string(),
input: serde_json::json!({"size": 10}),
result: Some(serde_json::json!({"decision": "inline"})),
source_location: Some("foo.rb:42".to_string()),
decision_id: Some(0xDEADBEEF),
};
let packed = rmp_serde::to_vec(&trace).expect("test");
let unpacked: DecisionTrace = rmp_serde::from_slice(&packed).expect("test");
assert_eq!(unpacked.timestamp_us, 12345);
assert_eq!(unpacked.category, "optimization");
assert_eq!(unpacked.decision_id, Some(0xDEADBEEF));
}
#[test]
fn test_read_decisions_from_msgpack_file() {
use std::io::Write;
use tempfile::NamedTempFile;
let traces = vec![
DecisionTrace {
timestamp_us: 100,
category: "type_inference".to_string(),
name: "infer_type".to_string(),
input: serde_json::json!({"var": "x"}),
result: Some(serde_json::json!({"type": "i32"})),
source_location: Some("foo.rb:1".to_string()),
decision_id: Some(generate_decision_id(
"type_inference",
"infer_type",
"foo.rb",
1,
)),
},
DecisionTrace {
timestamp_us: 200,
category: "optimization".to_string(),
name: "inline".to_string(),
input: serde_json::json!({"size": 5}),
result: Some(serde_json::json!({"decision": "yes"})),
source_location: Some("foo.rb:10".to_string()),
decision_id: Some(generate_decision_id("optimization", "inline", "foo.rb", 10)),
},
];
let mut temp_file = NamedTempFile::new().expect("test");
let packed = rmp_serde::to_vec(&traces).expect("test");
temp_file.write_all(&packed).expect("test");
temp_file.flush().expect("test");
let loaded = read_decisions_from_msgpack(temp_file.path()).expect("test");
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0].category, "type_inference");
assert_eq!(loaded[1].category, "optimization");
}
#[test]
fn test_read_decisions_from_msgpack_empty_file() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().expect("test");
let result = read_decisions_from_msgpack(temp_file.path());
assert!(result.is_err() || result.expect("test").is_empty());
}
#[test]
fn test_decision_tracer_write_to_msgpack() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let msgpack_path = temp_dir.path().join("decisions.msgpack");
let mut tracer = DecisionTracer::new();
tracer.add_decision_with_id(
"optimization",
"inline",
serde_json::json!({"size": 10}),
Some(serde_json::json!({"decision": "yes"})),
Some("foo.rb:42"),
Some(generate_decision_id("optimization", "inline", "foo.rb", 42)),
);
tracer.write_to_msgpack(&msgpack_path).expect("test");
let loaded = read_decisions_from_msgpack(&msgpack_path).expect("test");
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].category, "optimization");
assert_eq!(
loaded[0].decision_id,
Some(generate_decision_id("optimization", "inline", "foo.rb", 42))
);
}
#[test]
fn test_decision_tracer_generate_manifest() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let manifest_path = temp_dir.path().join("decision_manifest.json");
let mut tracer = DecisionTracer::new();
tracer.add_decision_with_id(
"type_inference",
"infer_var",
serde_json::json!({"var": "x"}),
Some(serde_json::json!({"type": "i32"})),
Some("test.rb:10"),
Some(generate_decision_id("type_inference", "infer_var", "test.rb", 10)),
);
tracer
.write_manifest(&manifest_path, "2.0.0", Some("abc123"), Some("3.213.0"))
.expect("test");
let manifest = DecisionManifest::load_from_file(&manifest_path).expect("test");
assert_eq!(manifest.version, "2.0.0");
assert_eq!(manifest.git_commit, Some("abc123".to_string()));
assert!(!manifest.entries.is_empty());
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_decision_id_deterministic(
category in "[a-z_]{1,20}",
name in "[a-z_]{1,20}",
file in "[a-z_./]{1,30}",
line in 1u32..10000u32
) {
let id1 = generate_decision_id(&category, &name, &file, line);
let id2 = generate_decision_id(&category, &name, &file, line);
assert_eq!(id1, id2, "Hash must be deterministic");
}
#[test]
fn prop_decision_id_different_categories_different_hashes(
name in "[a-z_]{1,20}",
file in "[a-z_./]{1,30}",
line in 1u32..1000u32
) {
let id_opt = generate_decision_id("optimization", &name, &file, line);
let id_type = generate_decision_id("type_inference", &name, &file, line);
assert_ne!(id_opt, id_type, "Different categories must produce different hashes");
}
#[test]
fn prop_decision_id_different_lines_different_hashes(
category in "[a-z_]{1,20}",
name in "[a-z_]{1,20}",
file in "[a-z_./]{1,30}",
line1 in 1u32..1000u32,
line2 in 1000u32..2000u32
) {
let id1 = generate_decision_id(&category, &name, &file, line1);
let id2 = generate_decision_id(&category, &name, &file, line2);
assert_ne!(id1, id2, "Different lines must produce different hashes");
}
#[test]
fn prop_decision_id_nonzero(
category in "[a-z_]{1,20}",
name in "[a-z_]{1,20}",
file in "[a-z_./]{1,30}",
line in 1u32..10000u32
) {
let id = generate_decision_id(&category, &name, &file, line);
assert_ne!(id, 0, "Hash should never be zero");
}
#[test]
fn prop_decision_id_uniform_distribution(
inputs in prop::collection::vec(
(
prop::string::string_regex("[a-z_]{1,20}").expect("test"),
prop::string::string_regex("[a-z_]{1,20}").expect("test"),
prop::string::string_regex("[a-z_./]{1,30}").expect("test"),
1u32..10000u32
),
100..200
)
) {
let mut hashes = std::collections::HashSet::new();
let mut collisions = 0;
for (category, name, file, line) in &inputs {
let id = generate_decision_id(category, name, file, *line);
if !hashes.insert(id) {
collisions += 1;
}
}
let collision_rate = (collisions as f64) / (inputs.len() as f64);
assert!(
collision_rate < 0.01,
"Collision rate too high: {:.2}% ({} collisions out of {} inputs)",
collision_rate * 100.0,
collisions,
inputs.len()
);
}
}
#[test]
fn test_hash_generation_performance() {
use std::time::Instant;
let iterations = 10000;
let start = Instant::now();
for i in 0..iterations {
let _ =
generate_decision_id("optimization", "inline_candidate", "foo.rb", i % 1000);
}
let elapsed = start.elapsed();
let avg_time_ns = elapsed.as_nanos() / (iterations as u128);
println!(
"Hash generation: {} iterations in {:?} (avg {} ns/hash)",
iterations, elapsed, avg_time_ns
);
assert!(
avg_time_ns < 5000,
"Hash generation too slow: {} ns (target < 5000 ns)",
avg_time_ns
);
}
#[test]
fn test_msgpack_serialization_performance() {
use std::time::Instant;
let mut traces = Vec::new();
for i in 0..1000 {
traces.push(DecisionTrace {
timestamp_us: i * 1000,
category: "optimization".to_string(),
name: "inline".to_string(),
input: serde_json::json!({"size": i % 100}),
result: Some(serde_json::json!({"decision": "yes"})),
source_location: Some(format!("foo.rb:{}", i % 500)),
decision_id: Some(generate_decision_id(
"optimization",
"inline",
"foo.rb",
(i % 500) as u32,
)),
});
}
let start = Instant::now();
let packed = rmp_serde::to_vec(&traces).expect("test");
let elapsed = start.elapsed();
println!(
"MessagePack serialization: 1000 traces in {:?} ({} bytes)",
elapsed,
packed.len()
);
assert!(
elapsed.as_millis() < 500,
"MessagePack serialization too slow: {:?} (target < 500ms)",
elapsed
);
}
#[test]
fn test_decision_tracer_full_v2_roundtrip() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let msgpack_path = temp_dir.path().join("decisions.msgpack");
let manifest_path = temp_dir.path().join("decision_manifest.json");
let mut tracer = DecisionTracer::new();
let decision_id_1 = generate_decision_id("optimization", "inline", "foo.rb", 10);
let decision_id_2 = generate_decision_id("type_inference", "infer_type", "bar.rb", 20);
tracer.add_decision_with_id(
"optimization",
"inline",
serde_json::json!({"size": 5}),
Some(serde_json::json!({"decision": "yes"})),
Some("foo.rb:10"),
Some(decision_id_1),
);
tracer.add_decision_with_id(
"type_inference",
"infer_type",
serde_json::json!({"var": "x"}),
Some(serde_json::json!({"type": "String"})),
Some("bar.rb:20"),
Some(decision_id_2),
);
tracer.write_to_msgpack(&msgpack_path).expect("test");
tracer.write_manifest(&manifest_path, "2.0.0", None, None).expect("test");
let loaded_traces = read_decisions_from_msgpack(&msgpack_path).expect("test");
let loaded_manifest = DecisionManifest::load_from_file(&manifest_path).expect("test");
assert_eq!(loaded_traces.len(), 2);
assert_eq!(loaded_manifest.version, "2.0.0");
assert_eq!(loaded_traces[0].decision_id, Some(decision_id_1));
assert_eq!(loaded_traces[1].decision_id, Some(decision_id_2));
}
#[test]
fn test_decision_categories_defined() {
use crate::decision_trace::categories::*;
assert_eq!(TYPE_INFERENCE, "type_inference");
assert!(TYPE_INFERENCE_FUNCTION.starts_with(TYPE_INFERENCE));
assert!(TYPE_INFERENCE_VARIABLE.starts_with(TYPE_INFERENCE));
assert!(TYPE_INFERENCE_COERCE.starts_with(TYPE_INFERENCE));
assert!(TYPE_INFERENCE_GENERIC.starts_with(TYPE_INFERENCE));
assert_eq!(OPTIMIZATION, "optimization");
assert!(OPTIMIZATION_INLINE.starts_with(OPTIMIZATION));
assert!(OPTIMIZATION_ESCAPE.starts_with(OPTIMIZATION));
assert!(OPTIMIZATION_TAIL_RECURSION.starts_with(OPTIMIZATION));
assert!(OPTIMIZATION_CONST_FOLDING.starts_with(OPTIMIZATION));
assert!(OPTIMIZATION_DEAD_CODE.starts_with(OPTIMIZATION));
assert_eq!(CODEGEN, "codegen");
assert!(CODEGEN_INTEGER_TYPE.starts_with(CODEGEN));
assert!(CODEGEN_STRING_STRATEGY.starts_with(CODEGEN));
assert!(CODEGEN_COLLECTION_TYPE.starts_with(CODEGEN));
assert!(CODEGEN_ERROR_HANDLING.starts_with(CODEGEN));
assert_eq!(STDLIB, "stdlib");
assert!(STDLIB_IO_MAPPING.starts_with(STDLIB));
assert!(STDLIB_STRING_METHOD.starts_with(STDLIB));
assert!(STDLIB_ARRAY_METHOD.starts_with(STDLIB));
}
#[test]
fn test_decision_categories_usage() {
use crate::decision_trace::categories::*;
let id1 = generate_decision_id(OPTIMIZATION, "inline_candidate", "foo.rb", 10);
let id2 = generate_decision_id(TYPE_INFERENCE, "infer_function", "bar.rb", 20);
assert_ne!(id1, id2);
assert_ne!(id1, 0);
assert_ne!(id2, 0);
}
#[test]
fn test_mmap_writer_create() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let mmap_path = temp_dir.path().join("decisions.msgpack");
let writer = MmapDecisionWriter::new(&mmap_path, 1024 * 1024).expect("test");
assert!(mmap_path.exists());
let metadata = std::fs::metadata(&mmap_path).expect("test");
assert_eq!(metadata.len(), 1024 * 1024);
drop(writer);
}
#[test]
fn test_mmap_writer_append_decision() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let mmap_path = temp_dir.path().join("decisions.msgpack");
let mut writer = MmapDecisionWriter::new(&mmap_path, 1024 * 1024).expect("test");
let decision = DecisionTrace {
timestamp_us: 1000,
category: "optimization".to_string(),
name: "inline".to_string(),
input: serde_json::json!({"size": 10}),
result: Some(serde_json::json!({"decision": "yes"})),
source_location: Some("foo.rb:42".to_string()),
decision_id: Some(generate_decision_id("optimization", "inline", "foo.rb", 42)),
};
writer.append(&decision).expect("test");
writer.flush().expect("test");
drop(writer);
let loaded = read_decisions_from_msgpack(&mmap_path).expect("test");
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].category, "optimization");
}
#[test]
fn test_mmap_writer_append_multiple() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let mmap_path = temp_dir.path().join("decisions.msgpack");
let mut writer = MmapDecisionWriter::new(&mmap_path, 1024 * 1024).expect("test");
for i in 0..100 {
let decision = DecisionTrace {
timestamp_us: i * 1000,
category: "optimization".to_string(),
name: "inline".to_string(),
input: serde_json::json!({"size": i}),
result: Some(serde_json::json!({"decision": "yes"})),
source_location: Some(format!("foo.rb:{}", i)),
decision_id: Some(generate_decision_id(
"optimization",
"inline",
"foo.rb",
i as u32,
)),
};
writer.append(&decision).expect("test");
}
writer.flush().expect("test");
drop(writer);
let loaded = read_decisions_from_msgpack(&mmap_path).expect("test");
assert_eq!(loaded.len(), 100);
}
#[test]
fn test_mmap_writer_no_blocking() {
use std::time::Instant;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let mmap_path = temp_dir.path().join("decisions.msgpack");
let mut writer = MmapDecisionWriter::new(&mmap_path, 10 * 1024 * 1024).expect("test");
let decision = DecisionTrace {
timestamp_us: 1000,
category: "optimization".to_string(),
name: "inline".to_string(),
input: serde_json::json!({"size": 10}),
result: Some(serde_json::json!({"decision": "yes"})),
source_location: Some("foo.rb:42".to_string()),
decision_id: Some(generate_decision_id("optimization", "inline", "foo.rb", 42)),
};
let start = Instant::now();
for _ in 0..1000 {
writer.append(&decision).expect("test");
}
let elapsed = start.elapsed();
println!(
"Mmap write: 1000 decisions in {:?} (avg {} ns/decision)",
elapsed,
elapsed.as_nanos() / 1000
);
assert!(
elapsed.as_micros() < 30000,
"Mmap write too slow: {:?} (target < 30ms debug, < 500us release)",
elapsed
);
}
#[test]
fn test_mmap_writer_auto_flush_on_drop() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("test");
let mmap_path = temp_dir.path().join("decisions.msgpack");
{
let mut writer = MmapDecisionWriter::new(&mmap_path, 1024 * 1024).expect("test");
let decision = DecisionTrace {
timestamp_us: 1000,
category: "optimization".to_string(),
name: "inline".to_string(),
input: serde_json::json!({"size": 10}),
result: Some(serde_json::json!({"decision": "yes"})),
source_location: Some("foo.rb:42".to_string()),
decision_id: Some(generate_decision_id("optimization", "inline", "foo.rb", 42)),
};
writer.append(&decision).expect("test");
}
let loaded = read_decisions_from_msgpack(&mmap_path).expect("test");
assert_eq!(loaded.len(), 1);
}
mod sprint28_sampling_tests {
use crate::decision_trace::sampling::*;
use serial_test::serial;
#[test]
fn test_fast_random_non_zero() {
for _ in 0..1000 {
let r = fast_random();
if r == 0 {
println!("Warning: fast_random() returned 0 (extremely unlikely)");
}
}
}
#[test]
fn test_fast_random_deterministic_per_thread() {
let r1 = fast_random();
let r2 = fast_random();
let r3 = fast_random();
assert_ne!(r1, r2);
assert_ne!(r2, r3);
assert_ne!(r1, r3);
}
#[test]
fn test_should_sample_trace_probability_zero() {
for _ in 0..1000 {
assert!(!should_sample_trace(0.0));
}
}
#[test]
#[serial]
fn test_should_sample_trace_probability_one() {
reset_trace_counter();
let count_before = get_trace_count();
let mut count = 0;
for _ in 0..100 {
if should_sample_trace(1.0) {
count += 1;
}
}
assert!(count >= 90, "Expected ~100 samples, got {}", count);
assert!(get_trace_count() >= count_before + 90);
}
#[test]
#[serial]
fn test_should_sample_trace_rate_limiter() {
reset_trace_counter();
for _ in 0..GLOBAL_TRACE_LIMIT {
assert!(should_sample_trace(1.0));
}
assert_eq!(get_trace_count(), GLOBAL_TRACE_LIMIT);
for _ in 0..100 {
assert!(!should_sample_trace(1.0));
}
assert_eq!(get_trace_count(), GLOBAL_TRACE_LIMIT);
}
#[test]
#[serial]
fn test_reset_trace_counter() {
reset_trace_counter();
for _ in 0..1000 {
should_sample_trace(1.0);
}
assert_eq!(get_trace_count(), 1000);
reset_trace_counter();
assert_eq!(get_trace_count(), 0);
assert!(should_sample_trace(1.0));
}
#[test]
#[serial]
fn test_sampling_rate_approximate() {
reset_trace_counter();
let probability = 0.1; let iterations = 10_000;
let mut sampled_count = 0;
for _ in 0..iterations {
if should_sample_trace(probability) {
sampled_count += 1;
}
}
let expected = (iterations as f64 * probability) as usize;
let lower_bound = (expected as f64 * 0.8) as usize;
let upper_bound = (expected as f64 * 1.2) as usize;
assert!(
sampled_count >= lower_bound && sampled_count <= upper_bound,
"Sampled {} out of {}, expected ~{} (range: {}-{})",
sampled_count,
iterations,
expected,
lower_bound,
upper_bound
);
}
#[test]
#[ignore = "Performance test - fails under coverage instrumentation"]
fn test_xorshift_performance() {
use std::time::Instant;
let iterations = 100_000;
let start = Instant::now();
for _ in 0..iterations {
let _ = fast_random();
}
let elapsed = start.elapsed();
let avg_ns = elapsed.as_nanos() / iterations;
println!(
"Xorshift performance: {} iterations in {:?} (avg {} ns/call)",
iterations, elapsed, avg_ns
);
assert!(
avg_ns < 50,
"Xorshift too slow: {} ns/call (target < 50ns with coverage)",
avg_ns
);
}
#[test]
#[serial]
#[ignore = "Performance test - fails under coverage instrumentation"]
fn test_sampling_decision_performance() {
use std::time::Instant;
reset_trace_counter();
let iterations = 100_000;
let start = Instant::now();
for _ in 0..iterations {
let _ = should_sample_trace(0.001); }
let elapsed = start.elapsed();
let avg_ns = elapsed.as_nanos() / iterations;
println!(
"Sampling decision performance: {} iterations in {:?} (avg {} ns/call)",
iterations, elapsed, avg_ns
);
assert!(
avg_ns < 50,
"Sampling decision too slow: {} ns/call (target < 50ns debug)",
avg_ns
);
}
}
mod sprint28_sampling_property_tests {
use crate::decision_trace::sampling::*;
use serial_test::serial;
use proptest::proptest;
proptest! {
#[test]
fn prop_xorshift_non_zero(iterations in 1usize..1000) {
let mut zero_count = 0;
for _ in 0..iterations {
if fast_random() == 0 {
zero_count += 1;
}
}
assert!(zero_count <= 1);
}
#[test]
#[serial]
fn prop_sampling_rate_bounded(probability in 0.0f64..=1.0f64) {
reset_trace_counter();
let iterations = 1000;
let mut sampled = 0;
for _ in 0..iterations {
if should_sample_trace(probability) {
sampled += 1;
}
}
assert!(sampled <= iterations);
assert!(get_trace_count() <= GLOBAL_TRACE_LIMIT * 2);
}
}
}
}
}