From 00b0fbaea9ba9b696e17c6822f28e007a3699afd Mon Sep 17 00:00:00 2001 From: pitboss Date: Wed, 20 May 2026 16:26:42 -0500 Subject: [PATCH] [pitboss] sweep after phase 20: 2 deferred items resolved --- src/dynamic/lang/rust.rs | 181 +++++++++++++++++++++++++++++++++++++-- src/dynamic/spec.rs | 59 +++++++++++++ 2 files changed, 232 insertions(+), 8 deletions(-) diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index cdb24b1f..ed0c9c8f 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -864,17 +864,23 @@ pub fn emit(spec: &HarnessSpec) -> Result { /// Phase 19 (Track M.1) — class-method harness for Rust. /// -/// Emits `src/main.rs` that constructs `entry::::default()` -/// and invokes `instance.(&payload)`. The fixture is -/// expected to derive `Default` on the receiver type so the harness -/// has a zero-arg construction path. When `Default` is unavailable -/// the fixture can provide a `new()` associated function; the -/// harness falls back to that via conditional compilation when -/// `Default` lookup fails. +/// Emits `src/main.rs` that constructs `entry::` and invokes +/// `instance.(&payload)`. The constructor pick is driven by +/// scanning the entry source for the receiver's construction shape: +/// when the class derives `Default` (or implements `Default` directly) +/// the harness emits `::default()`; otherwise it falls back to +/// `::new()`. This keeps the harness compilable against +/// non-Default fixtures without a separate emit path. fn emit_class_method_harness(spec: &HarnessSpec, class: &str, method: &str) -> HarnessSource { let shim = probe_shim(); let cargo_toml = generate_cargo_toml(spec.expected_cap); let entry_label = format!("{class}::{method}"); + let entry_src = read_entry_source(&spec.entry_file); + let ctor = if class_derives_default(&entry_src, class) { + "default" + } else { + "new" + }; let body = format!( r#"//! Nyx dynamic harness — class method (Phase 19 / Track M.1). mod entry; @@ -883,7 +889,7 @@ fn main() {{ let payload = nyx_payload(); let _ = &payload; __nyx_install_crash_guard("{entry_label}"); - let instance = entry::{class}::default(); + let instance = entry::{class}::{ctor}(); let _ = instance.{method}(&payload); }} @@ -942,6 +948,122 @@ fn b64_decode(input: &[u8]) -> Option> {{ } } +/// True when the entry source declares `class` as a type that derives +/// or implements `Default`. Two byte-level patterns are recognised: +/// +/// - `#[derive(...Default...)]` immediately preceding a `struct`/`enum` +/// declaration whose name matches `class`. +/// - An explicit `impl Default for ` block anywhere in the file. +/// +/// When neither is present the caller falls back to a `::new()` +/// ctor. The scan is conservative: unrecognised entry sources produce +/// `false` (so the harness emits `new()`), which keeps non-Default +/// fixtures compilable. +fn class_derives_default(entry_src: &str, class: &str) -> bool { + let impl_marker = format!("impl Default for {class}"); + if entry_src.contains(&impl_marker) { + return true; + } + let struct_marker = format!("struct {class}"); + let enum_marker = format!("enum {class}"); + let mut search_from = 0usize; + let bytes = entry_src.as_bytes(); + loop { + let struct_at = entry_src[search_from..].find(&struct_marker); + let enum_at = entry_src[search_from..].find(&enum_marker); + let (rel, marker_len) = match (struct_at, enum_at) { + (Some(s), Some(e)) if s <= e => (s, struct_marker.len()), + (Some(_), Some(e)) => (e, enum_marker.len()), + (Some(s), None) => (s, struct_marker.len()), + (None, Some(e)) => (e, enum_marker.len()), + (None, None) => return false, + }; + let decl_pos = search_from + rel; + let next_byte = bytes.get(decl_pos + marker_len).copied(); + let boundary_ok = matches!(next_byte, Some(b) if !b.is_ascii_alphanumeric() && b != b'_'); + if boundary_ok { + let window_start = decl_pos.saturating_sub(256); + let window = &entry_src[window_start..decl_pos]; + if let Some(derive_pos) = window.rfind("#[derive(") { + if let Some(end_rel) = window[derive_pos..].find(")]") { + let end = derive_pos + end_rel; + let derive_list = &window[derive_pos + "#[derive(".len()..end]; + let between = &window[end + ")]".len()..]; + // The derive attribute must directly precede the + // declaration — no other item / statement may sit + // between `#[derive(...)]` and the `struct` / + // `enum` token. Forbidden tokens (`;`, `{`, `}`, + // `=`, or another item keyword) signal the derive + // belongs to an earlier declaration. + let between_clean = strip_attrs_and_comments(between); + let forbidden = ['{', '}', ';', '=']; + let item_keyword = ["struct", "enum", "fn", "impl", "trait", "type", "mod"] + .iter() + .any(|kw| word_in_text(&between_clean, kw)); + let attaches_to_decl = !between_clean.chars().any(|c| forbidden.contains(&c)) + && !item_keyword; + if attaches_to_decl + && derive_list.split(',').any(|t| t.trim() == "Default") + { + return true; + } + } + } + } + search_from = decl_pos + 1; + } +} + +/// Drop `//` line comments and `#[...]` attribute blocks from `text`, +/// returning the remaining bytes joined by spaces. Used by +/// [`class_derives_default`] to decide whether the text between a +/// derive attribute and a declaration is empty (modulo visibility +/// modifiers and other attributes). +fn strip_attrs_and_comments(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + for line in text.lines() { + let mut s = line.trim(); + while s.starts_with("#[") { + if let Some(end) = s.find(']') { + s = s[end + 1..].trim_start(); + } else { + break; + } + } + if let Some(idx) = s.find("//") { + s = &s[..idx]; + } + out.push_str(s.trim()); + out.push(' '); + } + out +} + +/// True when `kw` appears in `text` as a whole word (ASCII word +/// boundaries on both sides). +fn word_in_text(text: &str, kw: &str) -> bool { + let bytes = text.as_bytes(); + let kw_bytes = kw.as_bytes(); + if kw_bytes.is_empty() { + return false; + } + let mut i = 0usize; + while i + kw_bytes.len() <= bytes.len() { + if &bytes[i..i + kw_bytes.len()] == kw_bytes { + let before_ok = i == 0 + || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_'; + let after_idx = i + kw_bytes.len(); + let after_ok = after_idx >= bytes.len() + || (!bytes[after_idx].is_ascii_alphanumeric() && bytes[after_idx] != b'_'); + if before_ok && after_ok { + return true; + } + } + i += 1; + } + false +} + /// Generate `Cargo.toml` for the harness crate. /// /// Dependencies are driven by `expected_cap`: @@ -1204,6 +1326,49 @@ mod tests { assert_eq!(harness.entry_subpath, Some("src/entry.rs".to_string())); } + #[test] + fn class_derives_default_matches_derive_attribute() { + let src = "#[derive(Default)]\npub struct UserService;"; + assert!(class_derives_default(src, "UserService")); + } + + #[test] + fn class_derives_default_matches_derive_among_other_traits() { + let src = "#[derive(Clone, Debug, Default, PartialEq)]\nstruct UserService { id: u32 }"; + assert!(class_derives_default(src, "UserService")); + } + + #[test] + fn class_derives_default_matches_explicit_impl() { + let src = "struct UserService;\nimpl Default for UserService { fn default() -> Self { Self } }"; + assert!(class_derives_default(src, "UserService")); + } + + #[test] + fn class_derives_default_matches_enum() { + let src = "#[derive(Default)]\nenum Mode { #[default] Off, On }"; + assert!(class_derives_default(src, "Mode")); + } + + #[test] + fn class_derives_default_false_when_absent() { + let src = "pub struct UserService { id: u32 }\nimpl UserService { pub fn new() -> Self { Self { id: 0 } } }"; + assert!(!class_derives_default(src, "UserService")); + } + + #[test] + fn class_derives_default_false_when_derive_on_different_type() { + let src = "#[derive(Default)]\nstruct OtherType;\npub struct UserService;"; + assert!(!class_derives_default(src, "UserService")); + } + + #[test] + fn class_derives_default_respects_word_boundary() { + // `struct UserServiceImpl` must not be treated as `UserService`. + let src = "#[derive(Default)]\nstruct UserServiceImpl;"; + assert!(!class_derives_default(src, "UserService")); + } + #[test] fn emit_env_var_slot() { let spec = make_spec(PayloadSlot::EnvVar("NYX_INPUT".into())); diff --git a/src/dynamic/spec.rs b/src/dynamic/spec.rs index fb3a0d54..8ee121b3 100644 --- a/src/dynamic/spec.rs +++ b/src/dynamic/spec.rs @@ -2216,6 +2216,65 @@ mod tests { ); } + /// Phase 20 (Track M.2) deferred-fix companion: when a real + /// `MessageHandler` adapter binds, the spec carries both the + /// `MessageHandler` variant on `entry_kind` and the broker + /// adapter id on `framework.adapter`. The Python emitter's + /// `python_broker_for_adapter` reads `framework.adapter` to + /// route the broker pick, and the `MessageHandler` short-circuit + /// reads `entry_kind` to dispatch — both fields must be + /// populated by `stamp_framework_binding` so real spec-derivation + /// matches the manual fixture path in `tests/message_handler_corpus.rs`. + #[test] + fn spec_attach_framework_binding_stamps_message_handler_and_sets_broker_adapter() { + let mut spec = HarnessSpec { + finding_id: "phase20stamp0001".into(), + entry_file: "src/consumer.py".into(), + entry_name: "on_message".into(), + entry_kind: EntryKind::Function, + lang: Lang::Python, + toolchain_id: "phase20".into(), + payload_slot: PayloadSlot::Param(0), + expected_cap: crate::labels::Cap::CODE_EXEC, + constraint_hints: vec![], + sink_file: "src/consumer.py".into(), + sink_line: 1, + spec_hash: "phase20stamp0001".into(), + derivation: SpecDerivationStrategy::FromFlowSteps, + stubs_required: vec![], + framework: None, + java_toolchain: JavaToolchain::default(), + }; + let pre_hash = spec.spec_hash.clone(); + + let binding = FrameworkBinding { + adapter: "kafka-python".to_owned(), + kind: EntryKind::MessageHandler { + queue: "orders".to_owned(), + message_schema: None, + }, + route: None, + request_params: vec![], + response_writer: None, + middleware: vec![], + }; + stamp_framework_binding(&mut spec, binding); + + assert_eq!( + spec.entry_kind.tag(), + crate::evidence::EntryKindTag::MessageHandler, + "MessageHandler variant must propagate from binding onto spec.entry_kind", + ); + if let EntryKind::MessageHandler { queue, .. } = &spec.entry_kind { + assert_eq!(queue, "orders"); + } else { + panic!("expected MessageHandler variant"); + } + let fw = spec.framework.as_ref().expect("framework must be set"); + assert_eq!(fw.adapter, "kafka-python"); + assert_ne!(pre_hash, spec.spec_hash); + } + /// Companion guard: when the binding carries a legacy unit /// variant (`Function` / `HttpRoute`), the stamping branch keeps /// `spec.entry_kind` and `spec.spec_hash` unchanged.