use std::collections::HashMap;
use tree_sitter::{Node, Parser};
use crate::index::{AnnotationKind, CoveredRegion};
use crate::walk::extract::{
make_annotation, AnnotationArgs, AnnotationForm, ExtractError, ExtractedAnnotation,
};
struct CDirective {
kind: AnnotationKind,
site: Option<String>,
args: AnnotationArgs,
}
#[aristo::intent(
"C annotations return in source order — top of file first — mirroring the \
Rust extractor's contract. The parse tree is walked top-to-bottom and \
results are pushed in encounter order; collecting through any unordered \
structure, or sorting, would break the stable index ordering the \
downstream fixtures index into positionally.",
verify = "test",
id = "extract_c_returns_annotations_in_source_order"
)]
pub fn extract_from_c_source(source: &str) -> Result<Vec<ExtractedAnnotation>, ExtractError> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_c::LANGUAGE.into())
.map_err(|e| ExtractError::CParse(format!("tree-sitter-c language load failed: {e}")))?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ExtractError::CParse("tree-sitter returned no parse tree".to_string()))?;
let root = tree.root_node();
let src_bytes = source.as_bytes();
let mut items: Vec<CItem> = Vec::new();
let mut by_name: HashMap<String, usize> = HashMap::new();
let mut item_at_row: HashMap<usize, usize> = HashMap::new();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if let Some(item) = c_item(&child, source) {
let idx = items.len();
by_name.entry(item.name.clone()).or_insert(idx);
item_at_row.insert(child.start_position().row, idx);
items.push(item);
}
}
let mut found = Vec::new();
let mut run: Vec<CPendingDirective> = Vec::new();
let mut last_directive_row: Option<usize> = None;
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() == "comment" {
if let Some(last) = last_directive_row {
if child.start_position().row != last + 1 {
resolve_run(
&run,
last_directive_row,
&items,
&by_name,
&item_at_row,
source,
&mut found,
);
run.clear();
last_directive_row = None;
}
}
let text = child.utf8_text(src_bytes).unwrap_or("");
if is_aristo_directive(text) {
if let Some(dir) = parse_directive(text) {
run.push(CPendingDirective {
dir,
start_row: child.start_position().row,
end_row: child.end_position().row,
});
}
last_directive_row = Some(child.end_position().row);
} else {
resolve_run(
&run,
last_directive_row,
&items,
&by_name,
&item_at_row,
source,
&mut found,
);
run.clear();
last_directive_row = None;
}
} else {
resolve_run(
&run,
last_directive_row,
&items,
&by_name,
&item_at_row,
source,
&mut found,
);
run.clear();
last_directive_row = None;
if child.kind() == "function_definition" {
if let Some(name) = c_function_name(&child, source) {
let site = format!("fn {name}");
if let Some(body) = child.child_by_field_name("body") {
walk_stmt_directives(&body, &site, &items, &by_name, source, &mut found);
}
}
}
}
}
resolve_run(
&run,
last_directive_row,
&items,
&by_name,
&item_at_row,
source,
&mut found,
);
Ok(found)
}
fn parse_directive(comment_text: &str) -> Option<CDirective> {
let body = comment_text.strip_prefix("//")?.trim_start();
let rest = body.strip_prefix("@aristo")?.trim_start();
let (kind, rest) = if let Some(r) = rest.strip_prefix("intent") {
(AnnotationKind::Intent, r)
} else if let Some(r) = rest.strip_prefix("assume") {
(AnnotationKind::Assume, r)
} else {
return None;
};
let rest = rest.trim();
let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
let parsed: CArgs = syn::parse_str(inner).ok()?;
Some(CDirective {
kind,
site: parsed.site,
args: parsed.args,
})
}
#[aristo::intent(
"`site` is a C-only target selector and is peeled off here, never entering \
the shared AnnotationArgs grammar (design decision Option B). Adding site \
to AnnotationArgs to `simplify` this would pollute the Rust contract with \
a field Rust has no use for — Rust attaches structurally and never needs \
an explicit target. site must be the first argument so this peel stays a \
single leading-token check instead of a full re-parse of the arg list.",
verify = "neural",
id = "c_site_selector_stays_out_of_shared_grammar"
)]
struct CArgs {
site: Option<String>,
args: AnnotationArgs,
}
impl syn::parse::Parse for CArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut site = None;
if input.peek(syn::Ident) && input.peek2(syn::Token![=]) {
let ident: syn::Ident = input.fork().parse()?;
if ident == "site" {
input.parse::<syn::Ident>()?;
input.parse::<syn::Token![=]>()?;
let lit: syn::LitStr = input.parse()?;
site = Some(lit.value());
input.parse::<syn::Token![,]>()?;
}
}
let args: AnnotationArgs = input.parse()?;
Ok(CArgs { site, args })
}
}
struct CPendingDirective {
dir: CDirective,
start_row: usize,
end_row: usize,
}
fn resolve_run(
run: &[CPendingDirective],
block_end_row: Option<usize>,
items: &[CItem],
by_name: &HashMap<String, usize>,
item_at_row: &HashMap<usize, usize>,
source: &str,
found: &mut Vec<ExtractedAnnotation>,
) {
if run.is_empty() {
return;
}
let adjacent = block_end_row.and_then(|r| item_at_row.get(&(r + 1)).copied());
for pending in run {
let target = match &pending.dir.site {
Some(name) => by_name.get(name).copied(),
None => adjacent,
};
if let Some(idx) = target {
if let Some(ann) =
build_c_annotation(&pending.dir, &items[idx], pending.start_row + 1, source)
{
found.push(ann);
}
}
}
}
fn build_c_annotation(
dir: &CDirective,
item: &CItem,
line: usize,
source: &str,
) -> Option<ExtractedAnnotation> {
let body_text = source
.get(item.body.start_byte()..item.body.end_byte())?
.to_string();
Some(make_annotation(
dir.kind,
AnnotationForm::Attribute,
dir.args.clone(),
&item.site,
line,
item.region,
body_text,
))
}
fn walk_stmt_directives(
block: &Node,
enclosing_site: &str,
items: &[CItem],
by_name: &HashMap<String, usize>,
source: &str,
found: &mut Vec<ExtractedAnnotation>,
) {
let mut run: Vec<CPendingDirective> = Vec::new();
let mut cursor = block.walk();
for child in block.named_children(&mut cursor) {
if child.kind() == "comment" {
match parse_directive(child.utf8_text(source.as_bytes()).unwrap_or("")) {
Some(dir) => {
if let Some(last) = run.last() {
if child.start_position().row != last.end_row + 1 {
resolve_stmt_run(
&run,
None,
enclosing_site,
items,
by_name,
source,
found,
);
run.clear();
}
}
run.push(CPendingDirective {
dir,
start_row: child.start_position().row,
end_row: child.end_position().row,
});
}
None => {
resolve_stmt_run(&run, None, enclosing_site, items, by_name, source, found);
run.clear();
}
}
} else {
resolve_stmt_run(
&run,
Some(child),
enclosing_site,
items,
by_name,
source,
found,
);
run.clear();
walk_stmt_directives(&child, enclosing_site, items, by_name, source, found);
}
}
resolve_stmt_run(&run, None, enclosing_site, items, by_name, source, found);
}
fn resolve_stmt_run(
run: &[CPendingDirective],
stmt: Option<Node>,
enclosing_site: &str,
items: &[CItem],
by_name: &HashMap<String, usize>,
source: &str,
found: &mut Vec<ExtractedAnnotation>,
) {
let Some(last) = run.last() else {
return;
};
let adjacent = stmt.filter(|s| s.start_position().row == last.end_row + 1);
for pending in run {
let line = pending.start_row + 1;
match &pending.dir.site {
Some(name) => {
if let Some(&idx) = by_name.get(name) {
if let Some(ann) = build_c_annotation(&pending.dir, &items[idx], line, source) {
found.push(ann);
}
}
}
None => {
if let Some(stmt) = adjacent {
if let Some(ann) =
build_c_stmt_annotation(&pending.dir, &stmt, enclosing_site, line, source)
{
found.push(ann);
}
}
}
}
}
}
fn build_c_stmt_annotation(
dir: &CDirective,
stmt: &Node,
enclosing_site: &str,
line: usize,
source: &str,
) -> Option<ExtractedAnnotation> {
let body_text = source.get(stmt.start_byte()..stmt.end_byte())?.to_string();
Some(make_annotation(
dir.kind,
AnnotationForm::Statement,
dir.args.clone(),
enclosing_site,
line,
CoveredRegion::Statement,
body_text,
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CInspectDirective {
pub type_name: String,
pub field: String,
pub ret: String,
pub with: Option<String>,
pub name: Option<String>,
pub line: usize,
}
impl CInspectDirective {
pub fn accessor_name(&self) -> String {
let suffix = self.name.as_deref().unwrap_or(&self.field);
format!("aristo_inspect_{}_{}", self.type_name, suffix)
}
}
struct InspectArgs {
field: String,
ret: String,
with: Option<String>,
name: Option<String>,
}
impl syn::parse::Parse for InspectArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let (mut field, mut ret, mut with, mut name) = (None, None, None, None);
while !input.is_empty() {
let key: syn::Ident = input.parse()?;
input.parse::<syn::Token![=]>()?;
let val: syn::LitStr = input.parse()?;
match key.to_string().as_str() {
"field" => field = Some(val.value()),
"ret" => ret = Some(val.value()),
"with" => with = Some(val.value()),
"name" => name = Some(val.value()),
other => {
return Err(syn::Error::new(
key.span(),
format!("unknown inspect arg `{other}`"),
))
}
}
if input.peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
}
Ok(InspectArgs {
field: field.ok_or_else(|| input.error("inspect requires `field`"))?,
ret: ret.ok_or_else(|| input.error("inspect requires `ret`"))?,
with,
name,
})
}
}
fn is_aristo_directive(comment_text: &str) -> bool {
comment_text
.strip_prefix("//")
.map(|b| b.trim_start().starts_with("@aristo"))
.unwrap_or(false)
}
fn strip_keyword<'a>(s: &'a str, kw: &str) -> Option<&'a str> {
let rest = s.strip_prefix(kw)?;
match rest.chars().next() {
Some(c) if c.is_alphanumeric() || c == '_' => None,
_ => Some(rest),
}
}
fn parse_inspect_directive(comment_text: &str) -> Option<Result<InspectArgs, String>> {
let body = comment_text.strip_prefix("//")?.trim_start();
let rest = body.strip_prefix("@aristo")?.trim_start();
let rest = strip_keyword(rest, "inspect")?.trim();
let inner = match rest.strip_prefix('(').and_then(|r| r.strip_suffix(')')) {
Some(inner) => inner,
None => return Some(Err("expected `inspect(...)`".to_string())),
};
Some(syn::parse_str::<InspectArgs>(inner).map_err(|e| e.to_string()))
}
#[aristo::intent(
"`complete` is false whenever ANY struct member could not be reduced to a \
plain field name — an anonymous union/struct member, a bitfield, or any \
shape this walker does not model. The unknown-field check MUST gate on \
`complete`: rejecting a field when the member list is only partially \
understood would fail a build on VALID code (a false negative), which for \
a codegen tool is worse than the silent-drop it replaced. Widening the \
set of members treated as \"understood\" without proving they are truly \
enumerable re-opens the false-reject hole.",
verify = "test",
id = "c_struct_field_completeness_gates_unknown_field_rejection"
)]
fn c_struct_field_names(body: &Node, source: &str) -> (Vec<String>, bool) {
let mut names = Vec::new();
let mut complete = true;
let mut cursor = body.walk();
for decl in body.named_children(&mut cursor) {
if decl.kind() != "field_declaration" {
complete = false;
continue;
}
let mut found_one = false;
let mut dc = decl.walk();
for child in decl.named_children(&mut dc) {
if let Some(n) = field_declarator_name(&child, source) {
names.push(n);
found_one = true;
}
}
if !found_one {
complete = false;
}
}
(names, complete)
}
fn field_declarator_name(node: &Node, source: &str) -> Option<String> {
let mut cur = *node;
for _ in 0..16 {
if cur.kind() == "field_identifier" {
return cur.utf8_text(source.as_bytes()).ok().map(|s| s.to_string());
}
cur = cur.child_by_field_name("declarator")?;
}
None
}
#[aristo::intent(
"inspect directives attach to the type on the line directly below a \
contiguous block of `// @aristo` directive lines — an intervening \
intent/assume directive does NOT break the block (adjacency is measured \
from the last aristo directive of any kind), but a plain comment or a \
blank-line gap does. This keeps a struct's intent and its inspect \
directives freely interleavable above it while a reformatter that \
inserts a blank line still (correctly) detaches them.",
verify = "test",
id = "extract_c_inspect_attaches_across_mixed_directive_block"
)]
pub fn extract_c_inspect_directives(source: &str) -> Result<Vec<CInspectDirective>, ExtractError> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_c::LANGUAGE.into())
.map_err(|e| ExtractError::CParse(format!("tree-sitter-c language load failed: {e}")))?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ExtractError::CParse("tree-sitter returned no parse tree".to_string()))?;
let root = tree.root_node();
let src_bytes = source.as_bytes();
let mut found = Vec::new();
let mut problems: Vec<String> = Vec::new();
let mut run: Vec<(InspectArgs, usize)> = Vec::new(); let mut last_directive_row: Option<usize> = None;
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() == "comment" {
if let Some(last) = last_directive_row {
if child.start_position().row != last + 1 {
run.clear();
}
}
let text = child.utf8_text(src_bytes).unwrap_or("");
if is_aristo_directive(text) {
match parse_inspect_directive(text) {
Some(Ok(args)) => run.push((args, child.start_position().row)),
Some(Err(msg)) => {
problems.push(format!("line {}: {msg}", child.start_position().row + 1))
}
None => {} }
last_directive_row = Some(child.end_position().row);
} else {
run.clear();
last_directive_row = None;
}
} else if let Some(item) = c_item(&child, source) {
if item.region == CoveredRegion::Type && !run.is_empty() {
if let Some(last) = last_directive_row {
if child.start_position().row == last + 1 {
let (fields, complete) = c_struct_field_names(&item.body, source);
for (args, start_row) in &run {
if complete && !fields.contains(&args.field) {
problems.push(format!(
"line {}: field `{}` is not declared in `{}`",
start_row + 1,
args.field,
item.name
));
continue;
}
found.push(CInspectDirective {
type_name: item.name.clone(),
field: args.field.clone(),
ret: args.ret.clone(),
with: args.with.clone(),
name: args.name.clone(),
line: start_row + 1,
});
}
}
}
}
run.clear();
last_directive_row = None;
} else {
run.clear();
last_directive_row = None;
}
}
if !problems.is_empty() {
return Err(ExtractError::CInspectInvalid(problems.join("\n")));
}
Ok(found)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CExposeDirective {
pub name: String,
pub signature: String,
pub line: usize,
}
fn parse_expose_directive(comment_text: &str) -> Option<Result<(), String>> {
let body = comment_text.strip_prefix("//")?.trim_start();
let rest = body.strip_prefix("@aristo")?.trim_start();
let rest = strip_keyword(rest, "expose")?.trim();
if rest.is_empty() {
return Some(Ok(()));
}
match rest.strip_prefix('(').and_then(|r| r.strip_suffix(')')) {
Some(inner) if inner.trim().is_empty() => Some(Ok(())),
Some(inner) => Some(Err(format!(
"the `expose({})` forwarder form is not yet supported; use bare \
`// @aristo expose` on a function marked `ARISTO_TU_LOCAL`",
inner.trim()
))),
None => Some(Err("expected `// @aristo expose` (bare)".to_string())),
}
}
fn c_function_signature(func: &Node, source: &str) -> Option<String> {
let body = func.child_by_field_name("body")?;
let sig = source.get(func.start_byte()..body.start_byte())?;
Some(sig.trim().to_string())
}
#[aristo::intent(
"The exposed prototype is the function's VERBATIM source signature (bytes \
up to the body brace), not a signature rebuilt from tree-sitter fields. A \
function carrying the `ARISTO_TU_LOCAL` macro prefix mis-parses into an \
ERROR node (the macro is read as the return type and the real return type \
as an error), so reconstructing `<type> <declarator>` from the tree would \
emit a WRONG prototype. Byte-range extraction is immune: the emitted \
`<signature>;` is exactly the real declaration, and `ARISTO_TU_LOCAL` \
expands to nothing when the harness compiles instrumented.",
verify = "test",
id = "expose_prototype_is_verbatim_signature_not_reconstructed"
)]
pub fn extract_c_expose_directives(source: &str) -> Result<Vec<CExposeDirective>, ExtractError> {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_c::LANGUAGE.into())
.map_err(|e| ExtractError::CParse(format!("tree-sitter-c language load failed: {e}")))?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ExtractError::CParse("tree-sitter returned no parse tree".to_string()))?;
let root = tree.root_node();
let src_bytes = source.as_bytes();
let mut found = Vec::new();
let mut problems: Vec<String> = Vec::new();
let mut pending_expose = 0usize;
let mut last_directive_row: Option<usize> = None;
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if child.kind() == "comment" {
if let Some(last) = last_directive_row {
if child.start_position().row != last + 1 {
pending_expose = 0;
}
}
let text = child.utf8_text(src_bytes).unwrap_or("");
if is_aristo_directive(text) {
match parse_expose_directive(text) {
Some(Ok(())) => pending_expose += 1,
Some(Err(msg)) => {
problems.push(format!("line {}: {msg}", child.start_position().row + 1))
}
None => {}
}
last_directive_row = Some(child.end_position().row);
} else {
pending_expose = 0;
last_directive_row = None;
}
} else {
if child.kind() == "function_definition" && pending_expose > 0 {
if let Some(last) = last_directive_row {
if child.start_position().row == last + 1 {
match (
c_function_name(&child, source),
c_function_signature(&child, source),
) {
(Some(name), Some(signature)) => {
found.push(CExposeDirective {
name,
signature,
line: child.start_position().row + 1,
});
}
_ => problems.push(format!(
"line {}: could not resolve the exposed function's signature",
child.start_position().row + 1
)),
}
}
}
}
pending_expose = 0;
last_directive_row = None;
}
}
if !problems.is_empty() {
return Err(ExtractError::CInspectInvalid(problems.join("\n")));
}
Ok(found)
}
struct CItem<'t> {
name: String,
site: String,
body: Node<'t>,
region: CoveredRegion,
}
fn c_item<'t>(node: &Node<'t>, source: &str) -> Option<CItem<'t>> {
match node.kind() {
"function_definition" => {
let name = c_function_name(node, source)?;
let body = node.child_by_field_name("body")?;
Some(CItem {
site: format!("fn {name}"),
name,
body,
region: CoveredRegion::Function,
})
}
"struct_specifier" | "union_specifier" | "enum_specifier" => {
c_tagged_type_item(node, source)
}
"type_definition" => c_typedef_type_item(node, source),
_ => None,
}
}
fn c_tagged_type_item<'t>(spec: &Node<'t>, source: &str) -> Option<CItem<'t>> {
let keyword = type_keyword(spec)?;
let name = spec
.child_by_field_name("name")?
.utf8_text(source.as_bytes())
.ok()?
.to_string();
let body = spec.child_by_field_name("body")?;
Some(CItem {
site: format!("{keyword} {name}"),
name,
body,
region: CoveredRegion::Type,
})
}
fn c_typedef_type_item<'t>(td: &Node<'t>, source: &str) -> Option<CItem<'t>> {
let spec = td.child_by_field_name("type")?;
let keyword = type_keyword(&spec)?;
let body = spec.child_by_field_name("body")?;
let name = td
.child_by_field_name("declarator")?
.utf8_text(source.as_bytes())
.ok()?
.to_string();
Some(CItem {
site: format!("{keyword} {name}"),
name,
body,
region: CoveredRegion::Type,
})
}
fn type_keyword(spec: &Node) -> Option<&'static str> {
match spec.kind() {
"struct_specifier" => Some("struct"),
"union_specifier" => Some("union"),
"enum_specifier" => Some("enum"),
_ => None,
}
}
fn c_function_name(func: &Node, source: &str) -> Option<String> {
let mut cur = func.child_by_field_name("declarator")?;
for _ in 0..16 {
if cur.kind() == "function_declarator" {
let name_node = cur.child_by_field_name("declarator")?;
return declarator_identifier(&name_node, source);
}
cur = cur.child_by_field_name("declarator")?;
}
None
}
fn declarator_identifier(start: &Node, source: &str) -> Option<String> {
let mut cur = *start;
for _ in 0..16 {
if cur.kind() == "identifier" {
return cur.utf8_text(source.as_bytes()).ok().map(|s| s.to_string());
}
cur = cur.child_by_field_name("declarator")?;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::text_hash;
use crate::walk::extract::ParentRaw;
fn extract(s: &str) -> Vec<ExtractedAnnotation> {
extract_from_c_source(s).expect("test source must parse as C")
}
#[test]
fn extracts_intent_directive_above_function() {
let src = "\
// @aristo intent(\"adds one to the input\", verify = \"test\", id = \"add_one\")
int add_one(int x) { return x + 1; }
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].kind, AnnotationKind::Intent);
assert_eq!(ann[0].form, AnnotationForm::Attribute);
assert_eq!(ann[0].text, "adds one to the input");
assert_eq!(ann[0].verify.as_deref(), Some("\"test\""));
assert_eq!(ann[0].id.as_deref(), Some("add_one"));
assert_eq!(ann[0].site, "fn add_one");
assert_eq!(ann[0].covered_region, CoveredRegion::Function);
assert_eq!(ann[0].line, 1);
}
#[test]
fn extracts_assume_directive_has_no_verify() {
let src = "\
// @aristo assume(\"the OS zero-fills freshly mmapped pages\")
int reads_zeroed(void) { return 0; }
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].kind, AnnotationKind::Assume);
assert_eq!(ann[0].verify, None);
}
#[test]
fn parses_parent_singular_and_list() {
let src = "\
// @aristo intent(\"child\", parent = \"ancestor\")
int a(void) { return 0; }
// @aristo intent(\"multi\", parent = [\"one\", \"two\"])
int b(void) { return 0; }
";
let ann = extract(src);
assert_eq!(ann.len(), 2);
assert_eq!(ann[0].parent, Some(ParentRaw::Single("ancestor".into())));
assert_eq!(
ann[1].parent,
Some(ParentRaw::Multiple(vec!["one".into(), "two".into()]))
);
}
#[test]
fn resolves_name_of_pointer_returning_function() {
let src = "\
// @aristo intent(\"allocates a buffer of n bytes\")
char *make_buf(int n) { return 0; }
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].site, "fn make_buf");
}
#[test]
fn blank_line_between_directive_and_function_detaches() {
let src = "\
// @aristo intent(\"detached — a blank line breaks adjacency\")
int f(void) { return 0; }
";
assert!(extract(src).is_empty());
}
#[test]
fn non_directive_comment_is_ignored() {
let src = "\
// an ordinary comment
int f(void) { return 0; }
";
assert!(extract(src).is_empty());
}
#[test]
fn stacked_directives_both_attach() {
let src = "\
// @aristo intent(\"describes behaviour\")
// @aristo assume(\"relies on an external invariant\")
int f(void) { return 0; }
";
let ann = extract(src);
assert_eq!(ann.len(), 2, "a contiguous run of directives all attach");
assert_eq!(ann[0].kind, AnnotationKind::Intent);
assert_eq!(ann[1].kind, AnnotationKind::Assume);
assert_eq!(ann[0].site, "fn f");
assert_eq!(ann[1].site, "fn f");
}
#[test]
fn returns_annotations_in_source_order() {
let src = "\
// @aristo intent(\"first\")
int a(void) { return 0; }
// @aristo intent(\"second\")
int b(void) { return 0; }
// @aristo intent(\"third\")
int c(void) { return 0; }
";
let ann = extract(src);
assert_eq!(ann.len(), 3);
assert_eq!(ann[0].text, "first");
assert_eq!(ann[1].text, "second");
assert_eq!(ann[2].text, "third");
assert!(ann[0].line < ann[1].line);
assert!(ann[1].line < ann[2].line);
}
#[test]
fn source_without_directives_returns_empty() {
let src = "int plain(void) { return 42; }\n";
assert!(extract(src).is_empty());
}
#[test]
fn malformed_args_silently_skipped() {
let src = "\
// @aristo intent(no_text = \"x\")
int bad(void) { return 0; }
";
assert!(extract(src).is_empty());
}
#[test]
fn text_hash_is_populated() {
let ann = extract("// @aristo intent(\"hello\")\nint x(void) { return 0; }\n");
assert_eq!(ann[0].text_hash, text_hash("hello"));
}
#[test]
fn body_hash_changes_with_body_but_not_with_text() {
let a = extract("// @aristo intent(\"v1\")\nint f(void) { return 1; }\n");
let b = extract("// @aristo intent(\"v2\")\nint f(void) { return 1; }\n");
let c = extract("// @aristo intent(\"v1\")\nint f(void) { return 2; }\n");
assert_ne!(a[0].text_hash, b[0].text_hash);
assert_eq!(
a[0].body_hash, b[0].body_hash,
"body unchanged → hash stable"
);
assert_ne!(
a[0].body_hash, c[0].body_hash,
"body changed → hash changes"
);
}
#[test]
fn extracts_directive_on_tagged_struct() {
let src = "\
// @aristo intent(\"a live key maps to the newest record's location\", id = \"keydir_entry\")
struct Entry {
unsigned long file_id;
unsigned long offset;
};
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].site, "struct Entry");
assert_eq!(ann[0].covered_region, CoveredRegion::Type);
}
#[test]
fn extracts_directive_on_enum_and_union() {
let src = "\
// @aristo intent(\"record tag discriminates a put from a tombstone\")
enum RecordTag { PUT, TOMBSTONE };
// @aristo intent(\"payload is interpreted per the record tag\")
union Payload { long as_int; double as_float; };
";
let ann = extract(src);
assert_eq!(ann.len(), 2);
assert_eq!(ann[0].site, "enum RecordTag");
assert_eq!(ann[0].covered_region, CoveredRegion::Type);
assert_eq!(ann[1].site, "union Payload");
assert_eq!(ann[1].covered_region, CoveredRegion::Type);
}
#[test]
fn extracts_directive_on_typedef_struct() {
let src = "\
// @aristo intent(\"opaque store handle; single-writer\", id = \"db_handle\")
typedef struct {
int fd;
unsigned long next_seqno;
} Db;
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].site, "struct Db");
assert_eq!(ann[0].covered_region, CoveredRegion::Type);
}
#[test]
fn type_body_hash_changes_with_fields_but_not_directive_text() {
let a = extract("// @aristo intent(\"v1\")\nstruct S { int a; };\n");
let b = extract("// @aristo intent(\"v2\")\nstruct S { int a; };\n");
let c = extract("// @aristo intent(\"v1\")\nstruct S { int a; int b; };\n");
assert_ne!(a[0].text_hash, b[0].text_hash);
assert_eq!(a[0].body_hash, b[0].body_hash, "field list unchanged");
assert_ne!(a[0].body_hash, c[0].body_hash, "field added → hash changes");
}
#[test]
fn directive_on_a_plain_variable_declaration_does_not_attach() {
let src = "\
struct Foo { int a; };
// @aristo intent(\"not a definition\")
struct Foo x;
";
let ann = extract(src);
assert!(ann.is_empty());
}
#[test]
fn site_targets_a_function_not_adjacent() {
let src = "\
// @aristo intent(site = \"db_open\", \"open recovers the durable prefix\", verify = \"test\", id = \"recover\")
static int internal_state;
int db_open(const char *dir) { return 0; }
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].site, "fn db_open");
assert_eq!(ann[0].id.as_deref(), Some("recover"));
assert_eq!(ann[0].verify.as_deref(), Some("\"test\""));
}
#[test]
fn site_reaches_past_an_intervening_comment() {
let src = "\
// @aristo intent(site = \"clamp\", \"clamps the value into the range\")
// an ordinary doc comment that would break plain adjacency
int clamp(int x) { return x; }
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].site, "fn clamp");
}
#[test]
fn site_targets_a_type_by_name() {
let src = "\
// @aristo intent(site = \"Entry\", \"a live key maps to its newest record\")
int unrelated(void) { return 0; }
typedef struct { unsigned long off; } Entry;
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].site, "struct Entry");
assert_eq!(ann[0].covered_region, CoveredRegion::Type);
}
#[test]
fn site_to_unknown_name_is_dropped() {
let src = "\
// @aristo intent(site = \"nope\", \"targets a nonexistent item\")
int real(void) { return 0; }
";
assert!(extract(src).is_empty());
}
#[test]
fn site_overrides_adjacency() {
let src = "\
// @aristo intent(\"attaches by adjacency\", id = \"adj\")
int alpha(void) { return 0; }
// @aristo intent(site = \"alpha\", \"overrides adjacency to target alpha\", id = \"exp\")
int beta(void) { return 0; }
";
let ann = extract(src);
assert_eq!(ann.len(), 2);
assert_eq!(ann[0].id.as_deref(), Some("adj"));
assert_eq!(ann[0].site, "fn alpha");
assert_eq!(ann[1].id.as_deref(), Some("exp"));
assert_eq!(ann[1].site, "fn alpha");
}
#[test]
fn extracts_stmt_directive_before_a_loop() {
let src = "\
int checksum(const char *buf, int n) {
int sum = 0;
// @aristo intent(\"each byte contributes once; no index is read twice\", verify = \"test\")
for (int i = 0; i < n; i++) {
sum += buf[i];
}
return sum;
}
";
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].form, AnnotationForm::Statement);
assert_eq!(ann[0].covered_region, CoveredRegion::Statement);
assert_eq!(ann[0].site, "fn checksum", "site is the enclosing function");
}
#[test]
fn extracts_stmt_directive_nested_in_a_loop_body() {
let src = "\
int f(int n) {
for (int i = 0; i < n; i++) {
// @aristo intent(\"the accumulator never overflows for valid n\")
long acc = i;
(void) acc;
}
return 0;
}
";
let ann = extract(src);
assert_eq!(
ann.len(),
1,
"a directive nested in a loop body must attach"
);
assert_eq!(ann[0].form, AnnotationForm::Statement);
assert_eq!(ann[0].site, "fn f");
}
#[test]
fn stmt_directive_body_hash_tracks_the_statement() {
let a = extract("int f() {\n// @aristo intent(\"x\")\nint y = 1;\nreturn y;\n}\n");
let b = extract("int f() {\n// @aristo intent(\"x\")\nint y = 2;\nreturn y;\n}\n");
assert_eq!(a.len(), 1);
assert_ne!(
a[0].body_hash, b[0].body_hash,
"the covered statement changed"
);
}
#[test]
fn function_and_statement_directives_come_out_in_source_order() {
let src = "\
// @aristo intent(\"function-level\", id = \"fn_level\")
int g(int n) {
// @aristo intent(\"statement-level\", id = \"stmt_level\")
int t = n;
return t;
}
";
let ann = extract(src);
assert_eq!(ann.len(), 2);
assert_eq!(ann[0].id.as_deref(), Some("fn_level"));
assert_eq!(ann[1].id.as_deref(), Some("stmt_level"));
assert!(ann[0].line < ann[1].line);
}
#[test]
fn block_comment_is_not_a_directive() {
let src = "\
/* @aristo intent(\"block comments are not directives\") */
int f(void) { return 0; }
";
assert!(extract(src).is_empty());
}
fn inspect(s: &str) -> Vec<CInspectDirective> {
extract_c_inspect_directives(s).expect("test source must parse as C")
}
#[test]
fn extracts_clone_inspect_on_typedef_struct() {
let src = "\
// @aristo inspect(field = \"next_seqno\", ret = \"uint64_t\")
typedef struct {
int fd;
unsigned long next_seqno;
} Db;
";
let d = inspect(src);
assert_eq!(d.len(), 1);
assert_eq!(d[0].type_name, "Db");
assert_eq!(d[0].field, "next_seqno");
assert_eq!(d[0].ret, "uint64_t");
assert_eq!(d[0].with, None); assert_eq!(d[0].accessor_name(), "aristo_inspect_Db_next_seqno");
}
#[test]
fn extracts_projection_inspect_with_and_name() {
let src = "\
// @aristo inspect(field = \"keydir\", ret = \"size_t\", with = \"keydir_live_count\", name = \"live_keys\")
struct Db { int keydir; };
";
let d = inspect(src);
assert_eq!(d.len(), 1);
assert_eq!(d[0].with.as_deref(), Some("keydir_live_count"));
assert_eq!(d[0].name.as_deref(), Some("live_keys"));
assert_eq!(d[0].accessor_name(), "aristo_inspect_Db_live_keys");
}
#[test]
fn multiple_inspects_stack_and_keep_source_order() {
let src = "\
// @aristo inspect(field = \"a\", ret = \"int\")
// @aristo inspect(field = \"b\", ret = \"long\")
struct S { int a; long b; };
";
let d = inspect(src);
assert_eq!(d.len(), 2);
assert_eq!(d[0].field, "a");
assert_eq!(d[1].field, "b");
assert!(d[0].line < d[1].line);
}
#[test]
fn inspect_attaches_across_an_interleaved_intent() {
let src = "\
// @aristo intent(\"the store handle; single-writer\", id = \"db_handle\")
// @aristo inspect(field = \"next_seqno\", ret = \"uint64_t\")
typedef struct { unsigned long next_seqno; } Db;
";
let d = inspect(src);
assert_eq!(d.len(), 1, "inspect after an intent still attaches");
assert_eq!(d[0].accessor_name(), "aristo_inspect_Db_next_seqno");
let ann = extract(src);
assert_eq!(ann.len(), 1);
assert_eq!(ann[0].id.as_deref(), Some("db_handle"));
}
#[test]
fn blank_line_detaches_inspect_from_the_struct() {
let src = "\
// @aristo inspect(field = \"a\", ret = \"int\")
struct S { int a; };
";
assert!(inspect(src).is_empty());
}
#[test]
fn plain_comment_breaks_the_inspect_block() {
let src = "\
// @aristo inspect(field = \"a\", ret = \"int\")
// an ordinary comment
struct S { int a; };
";
assert!(inspect(src).is_empty());
}
#[test]
fn inspect_missing_required_ret_is_a_hard_error() {
let src = "\
// @aristo inspect(field = \"a\")
struct S { int a; };
";
let err = extract_c_inspect_directives(src).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("line 1"), "error must name the line: {msg}");
assert!(
msg.contains("ret"),
"error must name the missing arg: {msg}"
);
}
#[test]
fn inspect_unknown_arg_is_a_hard_error() {
let src = "\
// @aristo inspect(field = \"a\", ret = \"int\", bogus = \"x\")
struct S { int a; };
";
assert!(extract_c_inspect_directives(src).is_err());
}
#[test]
fn inspect_unknown_field_is_a_hard_error() {
let src = "\
// @aristo inspect(field = \"nope\", ret = \"int\")
struct S { int a; long b; };
";
let err = extract_c_inspect_directives(src).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("nope") && msg.contains('S'), "got: {msg}");
}
#[test]
fn inspect_valid_field_passes_field_check() {
let src = "\
// @aristo inspect(field = \"b\", ret = \"long\")
struct S { int a; long b; };
";
let d = inspect(src);
assert_eq!(d.len(), 1);
assert_eq!(d[0].field, "b");
}
#[test]
fn inspect_field_check_is_conservative_on_pointer_fields() {
let src = "\
// @aristo inspect(field = \"buf\", ret = \"const char *\")
struct S { char *buf; unsigned long len; };
";
let d = inspect(src);
assert_eq!(d.len(), 1, "pointer field must be recognized as declared");
}
#[test]
fn inspect_directives_do_not_leak_into_the_index() {
let src = "\
// @aristo inspect(field = \"a\", ret = \"int\")
struct S { int a; };
";
assert!(
extract(src).is_empty(),
"inspect is not an index annotation"
);
}
#[test]
fn inspectfoo_is_not_an_inspect_directive() {
assert!(parse_inspect_directive("// @aristo inspectfoo(x = \"1\")").is_none());
}
fn expose(s: &str) -> Vec<CExposeDirective> {
extract_c_expose_directives(s).expect("test source must parse as C")
}
#[test]
fn exposes_a_tu_local_function_verbatim_signature() {
let src = "\
// @aristo expose
ARISTO_TU_LOCAL int recover_replay(Db *db) { return 0; }
";
let d = expose(src);
assert_eq!(d.len(), 1);
assert_eq!(d[0].name, "recover_replay");
assert_eq!(d[0].signature, "ARISTO_TU_LOCAL int recover_replay(Db *db)");
}
#[test]
fn exposes_a_plain_static_function() {
let src = "\
// @aristo expose
static long tally(const char *buf, int n) { return 0; }
";
let d = expose(src);
assert_eq!(d.len(), 1);
assert_eq!(d[0].signature, "static long tally(const char *buf, int n)");
}
#[test]
fn expose_forwarder_form_is_a_hard_error() {
let src = "\
// @aristo expose(as = \"recover_for_test\")
ARISTO_TU_LOCAL int recover_replay(Db *db) { return 0; }
";
let err = extract_c_expose_directives(src).unwrap_err();
assert!(err.to_string().contains("forwarder"), "got: {err}");
}
#[test]
fn expose_without_a_function_below_is_dropped() {
let src = "\
// @aristo expose
int detached(void) { return 0; }
";
assert!(expose(src).is_empty());
}
#[test]
fn expose_ignores_inspect_and_index_directives() {
let src = "\
// @aristo intent(\"does a thing\", id = \"x\")
int f(void) { return 0; }
// @aristo inspect(field = \"a\", ret = \"int\")
struct S { int a; };
";
assert!(expose(src).is_empty());
}
}