use crate::ast::Stmt;
#[must_use]
pub fn solana_syscall_signature(name: &str) -> Option<&'static str> {
match name {
"sol_log_" => Some("sol_log_(msg: *const u8, len: u64)"),
"sol_log_64_" => Some("sol_log_64_(a: u64, b: u64, c: u64, d: u64, e: u64)"),
"sol_log_pubkey" => Some("sol_log_pubkey(pubkey: *const Pubkey)"),
"sol_log_compute_units_" => Some("sol_log_compute_units_()"),
"sol_log_data" => Some("sol_log_data(data: *const SliceArray, n: u64)"),
"sol_memcpy_" => Some("sol_memcpy_(dst: *mut u8, src: *const u8, n: u64)"),
"sol_memset_" => Some("sol_memset_(dst: *mut u8, value: u8, n: u64)"),
"sol_memcmp_" => Some("sol_memcmp_(a: *const u8, b: *const u8, n: u64) -> i32"),
"sol_memmove_" => Some("sol_memmove_(dst: *mut u8, src: *const u8, n: u64)"),
"sol_invoke_signed_rust" => Some(
"sol_invoke_signed_rust(insn: *const Instruction, account_infos: *const AccountInfo, \
account_infos_len: u64, signers_seeds: *const SignerSeeds, signers_seeds_len: u64) -> u64",
),
"sol_invoke_signed_c" => Some(
"sol_invoke_signed_c(insn: *const Instruction, account_infos: *const AccountInfo, \
account_infos_len: u64, signers_seeds: *const SignerSeeds, signers_seeds_len: u64) -> u64",
),
"sol_try_find_program_address" => Some(
"sol_try_find_program_address(seeds: *const Seed, seeds_len: u64, \
program_id: *const Pubkey, address: *mut Pubkey, bump: *mut u8) -> u64",
),
"sol_create_program_address" => Some(
"sol_create_program_address(seeds: *const Seed, seeds_len: u64, \
program_id: *const Pubkey, address: *mut Pubkey) -> u64",
),
"sol_get_clock_sysvar" => Some("sol_get_clock_sysvar(buf: *mut Clock) -> u64"),
"sol_get_rent_sysvar" => Some("sol_get_rent_sysvar(buf: *mut Rent) -> u64"),
"sol_get_epoch_schedule_sysvar" => {
Some("sol_get_epoch_schedule_sysvar(buf: *mut EpochSchedule) -> u64")
}
"sol_get_fees_sysvar" => Some("sol_get_fees_sysvar(buf: *mut Fees) -> u64"),
"sol_get_stack_height" => Some("sol_get_stack_height() -> u64"),
"sol_get_return_data" => {
Some("sol_get_return_data(data: *mut u8, len: u64, program_id: *mut Pubkey) -> u64")
}
"sol_set_return_data" => Some("sol_set_return_data(data: *const u8, len: u64)"),
"sol_sha256" => {
Some("sol_sha256(slices: *const Slice, n: u64, result: *mut [u8; 32]) -> u64")
}
"sol_keccak256" => {
Some("sol_keccak256(slices: *const Slice, n: u64, result: *mut [u8; 32]) -> u64")
}
"sol_secp256k1_recover" => Some(
"sol_secp256k1_recover(hash: *const u8, recovery_id: u64, signature: *const u8, \
result: *mut u8) -> u64",
),
"sol_blake3" => {
Some("sol_blake3(slices: *const Slice, n: u64, result: *mut [u8; 32]) -> u64")
}
"sol_curve_validate_point" => {
Some("sol_curve_validate_point(curve: u64, point: *const u8) -> u64")
}
"sol_curve_group_op" => Some(
"sol_curve_group_op(curve: u64, op: u64, a: *const u8, b: *const u8, \
result: *mut u8) -> u64",
),
"sol_alloc_free_" => Some("sol_alloc_free_(size: u64, freep: *mut u8) -> *mut u8"),
"sol_panic_" => Some("sol_panic_(filename: *const u8, len: u64, line: u64, column: u64)"),
"sol_remaining_compute_units" => Some("sol_remaining_compute_units() -> u64"),
"abort" => Some("abort() -> !"),
_ => None,
}
}
const TAG: &str = "[solana]";
#[must_use]
pub fn solana_semantic_comment(name: &str, args: &[Option<String>; 5]) -> Option<String> {
match name {
"sol_memcmp_" => {
args[2].as_deref().and_then(parse_int_arg).map(|n| match n {
32 => format!("{TAG} 32-byte compare — likely Pubkey equality check"),
8 => format!("{TAG} 8-byte compare — likely discriminator / u64 check"),
16 => format!("{TAG} 16-byte compare — likely u128 / hash-prefix check"),
64 => format!("{TAG} 64-byte compare — likely signature / hash check"),
n => format!("{TAG} {n}-byte memcmp"),
})
}
"sol_try_find_program_address" => Some(format!(
"{TAG} PDA derivation — derives Pubkey + bump from seeds + program_id"
)),
"sol_create_program_address" => Some(format!(
"{TAG} PDA derivation (no-bump) — derives Pubkey from seeds + program_id"
)),
"sol_invoke_signed_rust" | "sol_invoke_signed_c" => Some(format!(
"{TAG} CPI — signed cross-program invocation (program acts as PDA signer)"
)),
"sol_get_clock_sysvar" => Some(format!(
"{TAG} sysvar fetch — Clock (slot, unix_timestamp, epoch, …)"
)),
"sol_get_rent_sysvar" => Some(format!("{TAG} sysvar fetch — Rent (exemption thresholds)")),
"sol_get_epoch_schedule_sysvar" => Some(format!("{TAG} sysvar fetch — EpochSchedule")),
"sol_get_fees_sysvar" => Some(format!("{TAG} sysvar fetch — Fees")),
"sol_set_return_data" => Some(format!(
"{TAG} sets program return data (visible to caller of this CPI)"
)),
"sol_get_return_data" => Some(format!(
"{TAG} reads return data set by the most recent CPI callee"
)),
"sol_secp256k1_recover" => Some(format!(
"{TAG} secp256k1 signature recovery — outputs the recovered pubkey"
)),
"sol_sha256" | "sol_keccak256" | "sol_blake3" => {
Some(format!("{TAG} cryptographic hash — output is 32 bytes"))
}
"sol_log_data" => Some(format!(
"{TAG} emits structured log data (visible in tx logs / event consumers)"
)),
_ => None,
}
}
pub fn annotate_pda_verify(body: &mut Vec<Stmt>) {
annotate_pda_verify_in(body);
}
const PDA_VERIFY_WINDOW: usize = 30;
fn annotate_pda_verify_in(body: &mut Vec<Stmt>) {
for stmt in body.iter_mut() {
match stmt {
Stmt::IfBlock {
then_body,
else_body,
..
} => {
annotate_pda_verify_in(then_body);
annotate_pda_verify_in(else_body);
}
Stmt::WhileBlock { body: wb, .. } => annotate_pda_verify_in(wb),
_ => {}
}
}
let mut hits: Vec<usize> = Vec::new();
for (i, stmt) in body.iter().enumerate() {
if !is_pda_derive_marker(stmt) {
continue;
}
let end = body.len().min(i + 1 + PDA_VERIFY_WINDOW);
for (j, candidate) in body.iter().enumerate().take(end).skip(i + 1) {
if is_pubkey_memcmp_marker(candidate) {
hits.push(j);
break;
}
}
}
for hit in hits.into_iter().rev() {
body.insert(
hit,
Stmt::Comment(format!(
"{TAG} >>> PDA verification check: derived PDA being compared against passed-in pubkey"
)),
);
}
}
fn is_pda_derive_marker(stmt: &Stmt) -> bool {
matches!(stmt, Stmt::Comment(s) if s.contains("PDA derivation"))
}
pub fn annotate_handler_banners(body: &mut Vec<Stmt>) {
annotate_handler_banners_in(body);
}
fn annotate_handler_banners_in(body: &mut Vec<Stmt>) {
for stmt in body.iter_mut() {
match stmt {
Stmt::IfBlock {
then_body,
else_body,
..
} => {
annotate_handler_banners_in(then_body);
annotate_handler_banners_in(else_body);
}
Stmt::WhileBlock { body: wb, .. } => annotate_handler_banners_in(wb),
_ => {}
}
}
let mut hits: Vec<(usize, String)> = Vec::new();
let mut last_seen: Option<String> = None;
for (i, stmt) in body.iter().enumerate() {
let detected = match stmt {
Stmt::Asm { text, .. } => extract_instruction_handler_from_lddw(text),
Stmt::Comment(s) => extract_instruction_handler(s),
_ => None,
};
if let Some(name) = detected {
if last_seen.as_deref() != Some(name.as_str()) {
hits.push((i, name.clone()));
last_seen = Some(name);
}
}
}
for (at, name) in hits.into_iter().rev() {
body.insert(at, Stmt::Comment(format!("{TAG} === handler: {name} ===")));
}
}
fn is_pubkey_memcmp_marker(stmt: &Stmt) -> bool {
matches!(stmt, Stmt::Comment(s) if s.contains("32-byte compare"))
}
#[must_use]
pub fn solana_function_summary(body: &[Stmt]) -> Option<String> {
let mut caps: Vec<String> = Vec::new();
walk_for_summary(body, &mut caps);
if caps.is_empty() {
return None;
}
caps.sort_unstable();
caps.dedup();
Some(format!("{TAG} function-summary: {}", caps.join(", ")))
}
fn walk_for_summary(body: &[Stmt], caps: &mut Vec<String>) {
for stmt in body {
match stmt {
Stmt::Asm { text, .. } if text.starts_with("lddw ") => {
if let Some(name) = extract_instruction_handler_from_lddw(text) {
caps.push(format!("handler:{name}"));
}
}
Stmt::Asm { text, .. } if text.starts_with("call ") => match text.as_str() {
"call sol_invoke_signed_rust" | "call sol_invoke_signed_c" => {
caps.push("cpi".into());
}
"call sol_try_find_program_address" | "call sol_create_program_address" => {
caps.push("pda-derive".into());
}
"call sol_get_clock_sysvar"
| "call sol_get_rent_sysvar"
| "call sol_get_epoch_schedule_sysvar"
| "call sol_get_fees_sysvar" => {
caps.push("sysvar".into());
}
"call sol_set_return_data" | "call sol_get_return_data" => {
caps.push("return-data".into());
}
"call sol_memcmp_" => caps.push("memcmp".into()),
"call sol_secp256k1_recover" => caps.push("secp256k1".into()),
"call sol_sha256" | "call sol_keccak256" | "call sol_blake3" => {
caps.push("hash".into());
}
"call sol_log_data" => caps.push("log-data".into()),
_ => {}
},
Stmt::Comment(s) => {
if let Some(name) = extract_instruction_handler(s) {
caps.push(format!("handler:{name}"));
}
}
Stmt::IfBlock {
then_body,
else_body,
..
} => {
walk_for_summary(then_body, caps);
walk_for_summary(else_body, caps);
}
Stmt::WhileBlock { body: wb, .. } => walk_for_summary(wb, caps),
_ => {}
}
}
}
fn parse_int_arg(text: &str) -> Option<u64> {
let t = text.trim();
if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
return u64::from_str_radix(hex, 16).ok();
}
t.parse::<u64>().ok()
}
#[must_use]
pub fn extract_instruction_handler_from_lddw(text: &str) -> Option<String> {
let body = text.strip_prefix("lddw ")?;
let (_reg, rest) = body.split_once(", ")?;
let inner = rest.strip_prefix('"')?.strip_suffix('"')?;
let after_prefix = inner.strip_prefix("Instruction: ")?;
let by_next_marker = after_prefix.find("Instruction:");
let by_param_open = after_prefix.find(" (");
let end = match (by_next_marker, by_param_open) {
(Some(a), Some(b)) => a.min(b),
(Some(a), None) => a,
(None, Some(b)) => b,
(None, None) => return None,
};
let name = after_prefix[..end].trim_end();
if name.is_empty() {
return None;
}
Some(name.to_string())
}
#[must_use]
pub fn extract_instruction_handler(comment: &str) -> Option<String> {
let body = comment.strip_prefix("→ sol_log_(")?.trim_end_matches(')');
let lit_start = body.find('"')? + 1;
let lit_end = lit_start
+ body[lit_start..]
.char_indices()
.find_map(|(i, c)| (c == '"').then_some(i))?;
let literal = &body[lit_start..lit_end];
let tail = body[lit_end + 1..].trim_start_matches(',').trim();
let len = parse_int_arg(tail.split(',').next()?.trim())? as usize;
let bytes = literal.as_bytes();
let effective_len = len.min(bytes.len());
let prefix_end = (0..=effective_len)
.rev()
.find(|&i| literal.is_char_boundary(i))?;
let prefix = &literal[..prefix_end];
let name = prefix.strip_prefix("Instruction: ")?;
let canonical = match name.find(" (") {
Some(at) => &name[..at],
None => name,
};
if canonical.is_empty() {
return None;
}
Some(canonical.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::unnecessary_wraps)]
fn slot(s: &str) -> Option<String> {
Some(s.to_string())
}
#[test]
fn memcmp_pubkey_size_is_flagged() {
let args = [None, None, slot("0x20"), None, None];
let c = solana_semantic_comment("sol_memcmp_", &args).unwrap();
assert!(c.contains("Pubkey equality check"), "{c}");
}
#[test]
fn memcmp_discriminator_size_is_flagged() {
let args = [None, None, slot("0x8"), None, None];
let c = solana_semantic_comment("sol_memcmp_", &args).unwrap();
assert!(c.contains("discriminator"), "{c}");
}
#[test]
fn memcmp_with_unknown_n_yields_no_comment() {
let args = [None, None, None, None, None];
assert!(solana_semantic_comment("sol_memcmp_", &args).is_none());
}
#[test]
fn cpi_call_is_tagged() {
let args: [Option<String>; 5] = Default::default();
let c = solana_semantic_comment("sol_invoke_signed_rust", &args).unwrap();
assert!(c.contains("CPI"));
}
#[test]
fn pda_verify_post_pass_inserts_annotation() {
let mut body = vec![
Stmt::Comment(format!("{TAG} PDA derivation — derives Pubkey + bump")),
Stmt::Asm {
text: "mov64 r1, r0".into(),
bytes: vec![],
},
Stmt::Comment(format!(
"{TAG} 32-byte compare — likely Pubkey equality check"
)),
];
annotate_pda_verify(&mut body);
assert_eq!(body.len(), 4);
match &body[2] {
Stmt::Comment(s) => {
assert!(s.contains("PDA verification check"), "{s}");
}
_ => panic!("expected a Comment at index 2; got {:?}", body[2]),
}
}
#[test]
fn pda_verify_post_pass_skips_when_no_memcmp_follows() {
let mut body = vec![Stmt::Comment(format!(
"{TAG} PDA derivation — derives Pubkey + bump"
))];
annotate_pda_verify(&mut body);
assert_eq!(body.len(), 1);
}
#[test]
fn parse_int_arg_decimal_and_hex() {
assert_eq!(parse_int_arg("32"), Some(32));
assert_eq!(parse_int_arg("0x20"), Some(32));
assert_eq!(parse_int_arg("local_50"), None);
}
#[test]
fn extract_handler_simple_with_params() {
let c = r#"→ sol_log_("Instruction: Stake (amount=more rodata blob…", 0x1b)"#;
assert_eq!(extract_instruction_handler(c), Some("Stake".to_string()));
}
#[test]
fn extract_handler_decimal_len() {
let c = r#"→ sol_log_("Instruction: ClaimRewardstrailing junk", 25)"#;
assert_eq!(
extract_instruction_handler(c),
Some("ClaimRewards".to_string())
);
}
#[test]
fn extract_handler_no_match_for_other_log_strings() {
let c = r#"→ sol_log_("error: bad input", 0x10)"#;
assert_eq!(extract_instruction_handler(c), None);
}
#[test]
fn extract_handler_no_match_for_non_log_comments() {
let c = "[solana] CPI — signed cross-program invocation";
assert_eq!(extract_instruction_handler(c), None);
}
#[test]
fn lddw_extract_param_boundary() {
let t = r#"lddw r1, "Instruction: Stake (amount=""#;
assert_eq!(
extract_instruction_handler_from_lddw(t),
Some("Stake".to_string())
);
}
#[test]
fn lddw_extract_first_of_concatenated_variants() {
let t = r#"lddw r1, "Instruction: CompleteUnstakeInstruction: CancelUnstakeRequestInstruction: CloseStakeAccountInstr""#;
assert_eq!(
extract_instruction_handler_from_lddw(t),
Some("CompleteUnstake".to_string())
);
}
#[test]
fn lddw_extract_refuses_truncated_blob_with_no_boundary() {
let t = r#"lddw r1, "Instruction: FixStakeAccountconnection reset) when slicing""#;
assert_eq!(extract_instruction_handler_from_lddw(t), None);
}
#[test]
fn lddw_extract_rejects_non_instruction_strings() {
let t = r#"lddw r1, "some other string""#;
assert_eq!(extract_instruction_handler_from_lddw(t), None);
}
#[test]
fn lddw_extract_rejects_non_lddw_asm() {
let t = r#"mov64 r1, "Instruction: Stake (a""#;
assert_eq!(extract_instruction_handler_from_lddw(t), None);
}
#[test]
fn function_summary_picks_up_handler_from_lddw() {
let body = vec![Stmt::Asm {
text: r#"lddw r1, "Instruction: InitializePool (tau=""#.into(),
bytes: vec![],
}];
let s = solana_function_summary(&body).unwrap();
assert!(s.contains("handler:InitializePool"), "{s}");
}
#[test]
fn function_summary_picks_up_handler() {
let body = vec![
Stmt::Asm {
text: "call sol_log_".into(),
bytes: vec![],
},
Stmt::Comment(r#"→ sol_log_("Instruction: Unstake (amount=more rodata", 0x1d)"#.into()),
];
let s = solana_function_summary(&body).unwrap();
assert!(s.contains("handler:Unstake"), "{s}");
assert!(!s.contains("handler:Unstake ("), "{s}");
}
#[test]
fn handler_banners_insert_above_each_marker() {
let mut body = vec![
Stmt::Asm {
text: r#"lddw r1, "Instruction: Stake (amount=""#.into(),
bytes: vec![],
},
Stmt::Asm {
text: "call sol_log_".into(),
bytes: vec![],
},
Stmt::Asm {
text: r#"lddw r1, "Instruction: Unstake (amount=""#.into(),
bytes: vec![],
},
];
annotate_handler_banners(&mut body);
assert_eq!(body.len(), 5);
match &body[0] {
Stmt::Comment(s) => assert!(s.contains("=== handler: Stake ==="), "{s}"),
_ => panic!("expected a banner Comment at index 0; got {:?}", body[0]),
}
match &body[3] {
Stmt::Comment(s) => assert!(s.contains("=== handler: Unstake ==="), "{s}"),
_ => panic!("expected a banner Comment at index 3; got {:?}", body[3]),
}
}
#[test]
fn handler_banners_dedup_consecutive_same_variant() {
let mut body = vec![
Stmt::Asm {
text: r#"lddw r1, "Instruction: ClaimRewardsInstruction: DepositRewards""#.into(),
bytes: vec![],
},
Stmt::Comment(r#"→ sol_log_("Instruction: ClaimRewardsInstruction:", 0x19)"#.into()),
];
annotate_handler_banners(&mut body);
let banners: Vec<&String> = body
.iter()
.filter_map(|s| match s {
Stmt::Comment(t) if t.contains("=== handler: ClaimRewards ===") => Some(t),
_ => None,
})
.collect();
assert_eq!(
banners.len(),
1,
"expected exactly one banner, got {body:?}"
);
}
}