1use crate::{Store, StoreError};
16use lex_ast::{CExpr, Stage};
17use serde::{Deserialize, Serialize};
18use std::collections::{BTreeMap, BTreeSet, HashSet};
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
23pub struct PlanPath {
24 pub chain: Vec<String>,
26 pub total_cost: u64,
30 pub fits: bool,
34 pub effects: BTreeSet<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Plan {
43 pub goal: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
47 pub session_id: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
51 pub remaining_budget: Option<i64>,
52 #[serde(skip_serializing_if = "Option::is_none")]
55 pub effective_cap: Option<u64>,
56 pub paths: Vec<PlanPath>,
58}
59
60struct FnInfo {
62 budget_cost: u64,
63 effects: BTreeSet<String>,
64 calls: Vec<String>,
65}
66
67impl Store {
68 pub fn plan(
70 &self,
71 branch: &str,
72 goal: &str,
73 max_cost: Option<u64>,
74 session_id: Option<&str>,
75 ) -> Result<Plan, StoreError> {
76 let head = self
78 .branch_head(branch)
79 .map_err(|e| StoreError::Io(std::io::Error::other(format!("branch_head: {e}"))))?;
80 let mut fns: BTreeMap<String, FnInfo> = BTreeMap::new();
81 for stage_id in head.values() {
82 let Ok(Stage::FnDecl(fd)) = self.get_ast(stage_id) else { continue };
83 let mut effects = BTreeSet::new();
84 let mut budget_cost: u64 = 0;
85 for e in &fd.effects {
86 if e.name == "budget" {
87 if let Some(lex_ast::EffectArg::Int { value }) = &e.arg {
88 budget_cost = budget_cost.saturating_add(*value as u64);
89 }
90 } else {
91 effects.insert(e.name.clone());
92 }
93 }
94 let mut calls = Vec::new();
95 collect_call_targets(&fd.body, &mut calls);
96 fns.entry(fd.name.clone()).or_insert(FnInfo {
97 budget_cost,
98 effects,
99 calls,
100 });
101 }
102
103 let (remaining_budget, session_id_out) = if let Some(sid) = session_id {
105 let sb = self.session_budget(sid)?;
106 (sb.remaining, Some(sid.to_string()))
107 } else {
108 (None, None)
109 };
110
111 let effective_cap: Option<u64> = match (max_cost, remaining_budget) {
113 (Some(m), Some(r)) => Some(m.min(r.max(0) as u64)),
114 (Some(m), None) => Some(m),
115 (None, Some(r)) => Some(r.max(0) as u64),
116 (None, None) => None,
117 };
118
119 let mut paths: Vec<PlanPath> = Vec::new();
120 if fns.contains_key(goal) {
121 expand_paths(goal, &fns, &mut Vec::new(), &mut HashSet::new(), &mut paths);
122 }
123 paths.sort_by(|a, b| {
125 a.total_cost
126 .cmp(&b.total_cost)
127 .then_with(|| a.chain.len().cmp(&b.chain.len()))
128 .then_with(|| a.chain.cmp(&b.chain))
129 });
130 for p in &mut paths {
132 p.fits = effective_cap.is_none_or(|cap| p.total_cost <= cap);
133 }
134
135 Ok(Plan {
136 goal: goal.to_string(),
137 session_id: session_id_out,
138 remaining_budget,
139 effective_cap,
140 paths,
141 })
142 }
143}
144
145fn collect_call_targets(expr: &CExpr, out: &mut Vec<String>) {
150 match expr {
151 CExpr::Call { callee, args } => {
152 if let CExpr::Var { name } = callee.as_ref() {
153 if !out.contains(name) {
154 out.push(name.clone());
155 }
156 }
157 collect_call_targets(callee, out);
158 for a in args {
159 collect_call_targets(a, out);
160 }
161 }
162 CExpr::Let { value, body, .. } => {
163 collect_call_targets(value, out);
164 collect_call_targets(body, out);
165 }
166 CExpr::Match { scrutinee, arms } => {
167 collect_call_targets(scrutinee, out);
168 for arm in arms {
169 collect_call_targets(&arm.body, out);
170 }
171 }
172 CExpr::Block { statements, result } => {
173 for s in statements {
174 collect_call_targets(s, out);
175 }
176 collect_call_targets(result, out);
177 }
178 CExpr::Constructor { args, .. } => {
179 for a in args {
180 collect_call_targets(a, out);
181 }
182 }
183 CExpr::RecordLit { fields } => {
184 for f in fields {
185 collect_call_targets(&f.value, out);
186 }
187 }
188 CExpr::TupleLit { items } | CExpr::ListLit { items } => {
189 for i in items {
190 collect_call_targets(i, out);
191 }
192 }
193 CExpr::FieldAccess { value, .. } => collect_call_targets(value, out),
194 CExpr::Lambda { body, .. } => collect_call_targets(body, out),
195 CExpr::BinOp { lhs, rhs, .. } => {
196 collect_call_targets(lhs, out);
197 collect_call_targets(rhs, out);
198 }
199 CExpr::UnaryOp { expr, .. } => collect_call_targets(expr, out),
200 CExpr::Return { value } => collect_call_targets(value, out),
201 CExpr::Literal { .. } | CExpr::Var { .. } => {}
202 }
203}
204
205fn expand_paths(
209 current: &str,
210 fns: &BTreeMap<String, FnInfo>,
211 chain: &mut Vec<String>,
212 visited: &mut HashSet<String>,
213 out: &mut Vec<PlanPath>,
214) {
215 chain.push(current.to_string());
216 let newly_inserted = visited.insert(current.to_string());
217
218 let Some(info) = fns.get(current) else {
219 emit_path(chain, fns, out);
222 chain.pop();
223 if newly_inserted {
224 visited.remove(current);
225 }
226 return;
227 };
228
229 let next: Vec<&String> = info
232 .calls
233 .iter()
234 .filter(|c| !visited.contains(*c))
235 .collect();
236 if next.is_empty() {
237 emit_path(chain, fns, out);
238 } else {
239 for callee in next {
240 expand_paths(callee, fns, chain, visited, out);
241 }
242 }
243
244 chain.pop();
245 if newly_inserted {
246 visited.remove(current);
247 }
248}
249
250fn emit_path(chain: &[String], fns: &BTreeMap<String, FnInfo>, out: &mut Vec<PlanPath>) {
251 let mut total_cost: u64 = 0;
252 let mut effects: BTreeSet<String> = BTreeSet::new();
253 for name in chain {
254 if let Some(info) = fns.get(name) {
255 total_cost = total_cost.saturating_add(info.budget_cost);
256 for e in &info.effects {
257 effects.insert(e.clone());
258 }
259 }
260 }
261 out.push(PlanPath {
262 chain: chain.to_vec(),
263 total_cost,
264 fits: true, effects,
266 });
267}