use std::sync::atomic::{AtomicU64, Ordering};
pub fn enabled() -> bool {
matches!(std::env::var("TF_STRUCTURAL_TRIGGER").as_deref(), Ok("1"))
}
#[derive(Debug, Default)]
pub struct StructuralCounters {
settled: AtomicU64,
closed: AtomicU64,
}
impl StructuralCounters {
pub fn new() -> Self {
Self::default()
}
pub fn record(&self, all_closed: bool) {
self.settled.fetch_add(1, Ordering::Relaxed);
if all_closed {
self.closed.fetch_add(1, Ordering::Relaxed);
}
}
pub fn snapshot(&self) -> (u64, u64) {
(
self.settled.load(Ordering::Relaxed),
self.closed.load(Ordering::Relaxed),
)
}
}
pub fn is_closed(src: &str) -> bool {
let b = src.as_bytes();
let n = b.len();
let mut i = 0usize;
let mut stack: Vec<u8> = Vec::new();
while i < n {
let c = b[i];
match c {
b'/' if i + 1 < n && b[i + 1] == b'/' => {
i += 2;
while i < n && b[i] != b'\n' {
i += 1;
}
}
b'/' if i + 1 < n && b[i + 1] == b'*' => {
let mut depth = 1usize;
i += 2;
while i < n && depth > 0 {
if i + 1 < n && b[i] == b'/' && b[i + 1] == b'*' {
depth += 1;
i += 2;
} else if i + 1 < n && b[i] == b'*' && b[i + 1] == b'/' {
depth -= 1;
i += 2;
} else {
i += 1;
}
}
if depth > 0 {
return false; }
}
b'r' | b'b' => {
let (is_raw, after_prefix) = classify_prefix(b, i);
if is_raw {
match scan_raw_string(b, after_prefix) {
Some(next) => i = next,
None => return false, }
} else if after_prefix > i {
i = after_prefix;
continue;
} else {
i = consume_ident(b, i);
}
}
b'"' => match scan_string(b, i + 1) {
Some(next) => i = next,
None => return false, },
b'\'' => match scan_char_or_lifetime(b, i) {
Some(next) => i = next,
None => return false, },
b'(' | b'[' | b'{' => {
stack.push(c);
i += 1;
}
b')' | b']' | b'}' => {
let want = match c {
b')' => b'(',
b']' => b'[',
_ => b'{',
};
match stack.pop() {
Some(open) if open == want => i += 1,
_ => return false, }
}
c if c == b'_' || c.is_ascii_alphabetic() => {
i = consume_ident(b, i);
}
_ => i += 1,
}
}
stack.is_empty()
}
fn classify_prefix(b: &[u8], i: usize) -> (bool, usize) {
let n = b.len();
if b[i] == b'b' && i + 1 < n && b[i + 1] == b'r' {
let j = i + 2;
if j < n && (b[j] == b'"' || b[j] == b'#') {
return (true, j);
}
}
if b[i] == b'r' && i + 1 < n {
if b[i + 1] == b'"' {
return (true, i + 1);
}
if b[i + 1] == b'#' {
let mut k = i + 1;
while k < n && b[k] == b'#' {
k += 1;
}
if k < n && b[k] == b'"' {
return (true, i + 1);
}
}
}
if b[i] == b'b' && i + 1 < n && (b[i + 1] == b'"' || b[i + 1] == b'\'') {
return (false, i + 1);
}
(false, i)
}
fn scan_raw_string(b: &[u8], start: usize) -> Option<usize> {
let n = b.len();
let mut k = start;
let mut hashes = 0usize;
while k < n && b[k] == b'#' {
hashes += 1;
k += 1;
}
if k >= n || b[k] != b'"' {
return None;
}
k += 1; while k < n {
if b[k] == b'"' {
let mut h = 0usize;
let mut p = k + 1;
while p < n && h < hashes && b[p] == b'#' {
h += 1;
p += 1;
}
if h == hashes {
return Some(p);
}
}
k += 1;
}
None
}
fn scan_string(b: &[u8], start: usize) -> Option<usize> {
let n = b.len();
let mut k = start;
while k < n {
match b[k] {
b'\\' => k += 2, b'"' => return Some(k + 1),
_ => k += 1,
}
}
None
}
fn scan_char_or_lifetime(b: &[u8], i: usize) -> Option<usize> {
let n = b.len();
if i + 1 >= n {
return None; }
if b[i + 1] == b'\\' {
let mut k = i + 3; while k < n {
match b[k] {
b'\\' => k += 2,
b'\'' => return Some(k + 1),
_ => k += 1,
}
}
return None; }
if b[i + 1] == b'_' || b[i + 1].is_ascii_alphabetic() {
let mut k = i + 1;
while k < n && (b[k] == b'_' || b[k].is_ascii_alphanumeric()) {
k += 1;
}
if k < n && b[k] == b'\'' {
return Some(k + 1); }
return Some(k); }
let mut k = i + 1;
while k < n {
match b[k] {
b'\\' => k += 2,
b'\'' => return Some(k + 1),
_ => k += 1,
}
}
None }
fn consume_ident(b: &[u8], i: usize) -> usize {
let n = b.len();
let mut k = i;
while k < n && (b[k] == b'_' || b[k].is_ascii_alphanumeric()) {
k += 1;
}
k.max(i + 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enabled_is_strict_one_default_off() {
fn rule(v: Option<&str>) -> bool {
v == Some("1")
}
assert!(rule(Some("1")));
assert!(!rule(None));
assert!(!rule(Some("")));
assert!(!rule(Some("0")));
assert!(!rule(Some("true")));
assert!(!rule(Some("yes")));
}
#[test]
fn balanced_simple() {
assert!(is_closed("fn main() { let x = (1 + [2, 3][0]); }"));
assert!(is_closed("")); assert!(is_closed("struct S;"));
}
#[test]
fn unbalanced_delims_are_open() {
assert!(!is_closed("fn main() {")); assert!(!is_closed("let x = (1 + 2;"));
assert!(!is_closed("foo)]}"));
assert!(!is_closed("{[(}])")); }
#[test]
fn delims_inside_strings_and_chars_dont_count() {
assert!(is_closed(r#"let s = "a { b ( c [ "; "#));
assert!(is_closed("let c = '}'; let d = '{';"));
assert!(is_closed(r#"println!("unbalanced ) ] }");"#));
}
#[test]
fn unterminated_string_is_open() {
assert!(!is_closed(r#"let s = "no end"#));
assert!(!is_closed("let s = \"line\nbroken")); }
#[test]
fn escapes_in_strings_and_chars() {
assert!(is_closed(r#"let s = "a \" b \\ c";"#));
assert!(is_closed(r#"let q = '\'';"#)); assert!(is_closed(r#"let n = '\n';"#));
}
#[test]
fn lifetimes_are_not_unterminated_chars() {
assert!(is_closed("fn f<'a>(x: &'a str) -> &'a str { x }"));
assert!(is_closed("struct S<'a, 'b> { a: &'a u8, b: &'b u8 }"));
assert!(is_closed("fn g() where 'static: 'static {}"));
}
#[test]
fn raw_strings_with_hashes() {
assert!(is_closed(r###"let s = r#"a "quote" and { ) ] inside"#;"###));
assert!(is_closed(r###"let s = r##"has "# inside"##;"###));
assert!(is_closed(r#"let s = r"no hashes { ( [ ";"#));
assert!(!is_closed("let s = r#\"never closed"));
}
#[test]
fn byte_strings_and_byte_chars() {
assert!(is_closed(r#"const M: &[u8] = b"tf-cas/input-hash/v1";"#));
assert!(is_closed("let z = b'}';"));
assert!(is_closed(r##"let r = br#"raw { byte ( ] str"#;"##));
assert!(is_closed(r###"let r = br##"has "# inside { ) ]"##;"###));
assert!(!is_closed(r#"let m = b"unterminated"#));
}
#[test]
fn raw_identifier_is_not_a_string() {
assert!(is_closed("let r#fn = 1; let r#match = 2;"));
assert!(!is_closed("fn r#async() {")); }
#[test]
fn comments_mask_delimiters() {
assert!(is_closed("fn f() {} // trailing ) ] } noise"));
assert!(is_closed("/* ( [ { unbalanced in comment */ fn f() {}"));
assert!(is_closed("/* outer /* nested */ still */ struct S;"));
assert!(!is_closed("fn f() {} /* unterminated comment ( ["));
}
#[test]
fn realistic_agent_whole_file_is_closed() {
let src = r#"
//! a module
use std::collections::BTreeMap;
pub struct Thing<'a> {
name: &'a str,
bytes: Vec<u8>,
}
impl<'a> Thing<'a> {
pub fn new(name: &'a str) -> Self {
Self { name, bytes: b"hdr/v1".to_vec() }
}
pub fn tag(&self) -> char { '#' }
}
#[cfg(test)]
mod tests {
#[test]
fn t() { assert_eq!(2 + 2, 4); }
}
"#;
assert!(is_closed(src));
}
#[test]
fn realistic_agent_midbatch_is_open() {
let src = "pub fn handler(req: Request) -> Response {\n let parsed = ";
assert!(!is_closed(src));
}
#[test]
fn counters_track_settled_and_closed() {
let c = StructuralCounters::new();
assert_eq!(c.snapshot(), (0, 0));
c.record(true);
c.record(false);
c.record(true);
assert_eq!(c.snapshot(), (3, 2)); }
}