use crate::parser::Expr;
pub fn effect_reason(op: &str) -> Option<&'static str> {
match op {
"set!" | "set" => Some("mutates a variable"),
"print" | "println" | "display" | "newline" => Some("performs I/O"),
"error" => Some("raises an error (a control-flow side effect)"),
"shell" | "shell-run" => Some("runs a shell command"),
"read-file" | "write-file" | "append-file" | "delete-file" | "file-exists" | "list-dir"
| "file-read" | "file-write" | "file-append" | "file-delete" | "file-exists?"
| "dir-create" | "dir-list" | "file-realpath" | "file-symlink?" | "file-hash"
| "checkpoint"
=> Some("touches the filesystem"),
"llm" | "tool-call" | "react-loop" => Some("calls an external LLM/tool"),
"remember" | "recall" | "forget" | "memory-list" => Some("reads or writes persistent memory"),
"kg-add!" | "kg-clear!" | "kg-query" | "kg-count" | "kg-triples"
| "kg-load-ntriples" | "kg-save-ntriples"
=> Some("reads or writes the knowledge graph"),
"gensym" => Some("non-deterministic — returns a different value each call"),
"load" | "load-relative" => Some("loads and executes another file"),
_ => None,
}
}
pub fn check(expr: &Expr, findings: &mut Vec<String>) {
if let Expr::List(items) = expr {
if items.is_empty() { return; }
if let Expr::Symbol(head) = &items[0] {
match head.as_str() {
"quote" => return, "quasiquote" if items.len() == 2 => { check_quasi(&items[1], findings); return; }
_ => {
if let Some(reason) = effect_reason(head) {
findings.push(format!("{}: {}", head, reason));
}
}
}
}
for item in items.iter() { check(item, findings); }
}
}
fn check_quasi(expr: &Expr, findings: &mut Vec<String>) {
if let Expr::List(items) = expr {
if let Some(Expr::Symbol(s)) = items.first() {
if (s == "unquote" || s == "unquote-splicing") && items.len() == 2 {
check(&items[1], findings); return;
}
}
for item in items.iter() { check_quasi(item, findings); }
}
}