1use std::collections::HashMap;
48
49use crate::fused::CostEstimate;
50use crate::kernel::OpParams;
51use fuel_ir::dispatch::OpKind;
52use fuel_ir::{DType, Shape};
53
54#[derive(Debug, Clone, PartialEq)]
59pub enum CompiledCostExpr {
60 Unknown,
64 Expr(CostNode),
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub enum CostNode {
71 Lit(f64),
73 Sym(String),
77 Bin {
79 op: BinOp,
80 lhs: Box<CostNode>,
81 rhs: Box<CostNode>,
82 },
83 Neg(Box<CostNode>),
85 Index {
89 base: Box<CostNode>,
90 index: Box<CostNode>,
91 },
92 Call { name: String, args: Vec<CostNode> },
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum BinOp {
101 Add,
102 Sub,
103 Mul,
104 Div,
105 Rem,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct CostParseError(pub String);
112
113impl std::fmt::Display for CostParseError {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.write_str(&self.0)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct CostEvalError(pub String);
122
123impl std::fmt::Display for CostEvalError {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.write_str(&self.0)
126 }
127}
128
129#[derive(Debug, Clone, PartialEq)]
134enum Tok {
135 Num(f64),
136 Ident(String),
137 Plus,
138 Minus,
139 Star,
140 Slash,
141 Percent,
142 LParen,
143 RParen,
144 LBracket,
145 RBracket,
146 Comma,
147}
148
149fn tokenize(src: &str) -> Result<Vec<Tok>, CostParseError> {
150 let mut toks = Vec::new();
151 let bytes: Vec<char> = src.chars().collect();
152 let mut i = 0;
153 while i < bytes.len() {
154 let c = bytes[i];
155 match c {
156 ' ' | '\t' | '\n' | '\r' => {
157 i += 1;
158 }
159 '+' => {
160 toks.push(Tok::Plus);
161 i += 1;
162 }
163 '-' => {
164 toks.push(Tok::Minus);
165 i += 1;
166 }
167 '*' => {
168 toks.push(Tok::Star);
169 i += 1;
170 }
171 '/' => {
172 toks.push(Tok::Slash);
173 i += 1;
174 }
175 '%' => {
176 toks.push(Tok::Percent);
177 i += 1;
178 }
179 '(' => {
180 toks.push(Tok::LParen);
181 i += 1;
182 }
183 ')' => {
184 toks.push(Tok::RParen);
185 i += 1;
186 }
187 '[' => {
188 toks.push(Tok::LBracket);
189 i += 1;
190 }
191 ']' => {
192 toks.push(Tok::RBracket);
193 i += 1;
194 }
195 ',' => {
196 toks.push(Tok::Comma);
197 i += 1;
198 }
199 c if c.is_ascii_digit() => {
200 let start = i;
201 let mut seen_dot = false;
202 while i < bytes.len()
203 && (bytes[i].is_ascii_digit() || (bytes[i] == '.' && !seen_dot))
204 {
205 if bytes[i] == '.' {
206 seen_dot = true;
207 }
208 i += 1;
209 }
210 let lit: String = bytes[start..i].iter().collect();
211 let val: f64 = lit
212 .parse()
213 .map_err(|_| CostParseError(format!("bad numeric literal `{lit}`")))?;
214 toks.push(Tok::Num(val));
215 }
216 c if c.is_ascii_alphabetic() || c == '_' => {
217 let start = i;
218 loop {
224 while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == '_') {
225 i += 1;
226 }
227 if i + 1 < bytes.len()
228 && bytes[i] == '.'
229 && (bytes[i + 1].is_ascii_alphabetic() || bytes[i + 1] == '_')
230 {
231 i += 1; continue;
233 }
234 break;
235 }
236 let ident: String = bytes[start..i].iter().collect();
237 toks.push(Tok::Ident(ident));
238 }
239 other => {
240 return Err(CostParseError(format!(
241 "unexpected character `{other}` in cost expression"
242 )));
243 }
244 }
245 }
246 Ok(toks)
247}
248
249struct Parser {
254 toks: Vec<Tok>,
255 pos: usize,
256}
257
258impl Parser {
259 fn peek(&self) -> Option<&Tok> {
260 self.toks.get(self.pos)
261 }
262
263 fn bump(&mut self) -> Option<Tok> {
264 let t = self.toks.get(self.pos).cloned();
265 if t.is_some() {
266 self.pos += 1;
267 }
268 t
269 }
270
271 fn parse_expr(&mut self) -> Result<CostNode, CostParseError> {
272 let mut lhs = self.parse_term()?;
273 while let Some(tok) = self.peek() {
274 let op = match tok {
275 Tok::Plus => BinOp::Add,
276 Tok::Minus => BinOp::Sub,
277 _ => break,
278 };
279 self.bump();
280 let rhs = self.parse_term()?;
281 lhs = CostNode::Bin {
282 op,
283 lhs: Box::new(lhs),
284 rhs: Box::new(rhs),
285 };
286 }
287 Ok(lhs)
288 }
289
290 fn parse_term(&mut self) -> Result<CostNode, CostParseError> {
291 let mut lhs = self.parse_factor()?;
292 while let Some(tok) = self.peek() {
293 let op = match tok {
294 Tok::Star => BinOp::Mul,
295 Tok::Slash => BinOp::Div,
296 Tok::Percent => BinOp::Rem,
297 _ => break,
298 };
299 self.bump();
300 let rhs = self.parse_factor()?;
301 lhs = CostNode::Bin {
302 op,
303 lhs: Box::new(lhs),
304 rhs: Box::new(rhs),
305 };
306 }
307 Ok(lhs)
308 }
309
310 fn parse_factor(&mut self) -> Result<CostNode, CostParseError> {
311 let atom = match self.bump() {
312 Some(Tok::Num(v)) => CostNode::Lit(v),
313 Some(Tok::Ident(name)) => {
314 if matches!(self.peek(), Some(Tok::LParen)) {
317 self.bump(); let mut args = Vec::new();
319 if !matches!(self.peek(), Some(Tok::RParen)) {
320 loop {
321 args.push(self.parse_expr()?);
322 match self.peek() {
323 Some(Tok::Comma) => {
324 self.bump();
325 }
326 _ => break,
327 }
328 }
329 }
330 match self.bump() {
331 Some(Tok::RParen) => {}
332 _ => {
333 return Err(CostParseError(
334 "unbalanced `(` in function call — expected `)`".into(),
335 ));
336 }
337 }
338 CostNode::Call { name, args }
339 } else {
340 CostNode::Sym(name)
341 }
342 }
343 Some(Tok::Minus) => {
344 let inner = self.parse_factor()?;
345 return Ok(CostNode::Neg(Box::new(inner)));
346 }
347 Some(Tok::LParen) => {
348 let inner = self.parse_expr()?;
349 match self.bump() {
350 Some(Tok::RParen) => inner,
351 _ => return Err(CostParseError("unbalanced `(` — expected `)`".into())),
352 }
353 }
354 Some(other) => {
355 return Err(CostParseError(format!(
356 "unexpected token `{other:?}` where a value was expected"
357 )));
358 }
359 None => {
360 return Err(CostParseError(
361 "unexpected end of cost expression (expected a value)".into(),
362 ));
363 }
364 };
365 let mut node = atom;
368 while matches!(self.peek(), Some(Tok::LBracket)) {
369 self.bump(); let index = self.parse_expr()?;
371 match self.bump() {
372 Some(Tok::RBracket) => {}
373 _ => return Err(CostParseError("unbalanced `[` — expected `]`".into())),
374 }
375 node = CostNode::Index {
376 base: Box::new(node),
377 index: Box::new(index),
378 };
379 }
380 Ok(node)
381 }
382}
383
384pub fn parse_expr(src: &str) -> Result<CostNode, CostParseError> {
389 let toks = tokenize(src)?;
390 if toks.is_empty() {
391 return Err(CostParseError("empty cost expression".into()));
392 }
393 let mut parser = Parser { toks, pos: 0 };
394 let node = parser.parse_expr()?;
395 if parser.pos != parser.toks.len() {
396 return Err(CostParseError(format!(
397 "trailing tokens after a complete expression (at token {})",
398 parser.pos
399 )));
400 }
401 Ok(node)
402}
403
404pub fn compile_field(src: Option<&str>) -> Result<CompiledCostExpr, CostParseError> {
408 match src {
409 None => Ok(CompiledCostExpr::Unknown),
410 Some(s) if s.trim().is_empty() => Ok(CompiledCostExpr::Unknown),
411 Some(s) => Ok(CompiledCostExpr::Expr(parse_expr(s)?)),
412 }
413}
414
415pub fn eval(
426 expr: &CompiledCostExpr,
427 bindings: &HashMap<String, f64>,
428) -> Result<f64, CostEvalError> {
429 match expr {
430 CompiledCostExpr::Unknown => Ok(0.0),
431 CompiledCostExpr::Expr(node) => eval_node(node, bindings),
432 }
433}
434
435fn eval_node(node: &CostNode, bindings: &HashMap<String, f64>) -> Result<f64, CostEvalError> {
436 match node {
437 CostNode::Lit(v) => Ok(*v),
438 CostNode::Sym(name) => bindings
439 .get(name)
440 .copied()
441 .ok_or_else(|| CostEvalError(format!("undefined cost symbol `{name}`"))),
442 CostNode::Index { base, index } => {
443 let key = canonical_key(node);
446 if let Some(v) = bindings.get(&key) {
447 return Ok(*v);
448 }
449 let _ = eval_node(base, bindings);
454 let _ = eval_node(index, bindings);
455 Err(CostEvalError(format!("undefined cost shape-index `{key}`")))
456 }
457 CostNode::Call { .. } => {
458 let key = canonical_key(node);
459 bindings
460 .get(&key)
461 .copied()
462 .ok_or_else(|| CostEvalError(format!("undefined cost term `{key}`")))
463 }
464 CostNode::Neg(inner) => Ok(-eval_node(inner, bindings)?),
465 CostNode::Bin { op, lhs, rhs } => {
466 let l = eval_node(lhs, bindings)?;
467 let r = eval_node(rhs, bindings)?;
468 match op {
469 BinOp::Add => Ok(l + r),
470 BinOp::Sub => Ok(l - r),
471 BinOp::Mul => Ok(l * r),
472 BinOp::Div => {
473 if r == 0.0 {
474 Err(CostEvalError("division by zero in cost expression".into()))
475 } else {
476 Ok(l / r)
477 }
478 }
479 BinOp::Rem => {
480 if r == 0.0 {
481 Err(CostEvalError("modulo by zero in cost expression".into()))
482 } else {
483 Ok(l % r)
484 }
485 }
486 }
487 }
488 }
489}
490
491pub fn bind_cost_symbols(
497 _op: OpKind,
498 shapes: &[Shape],
499 dtypes: &[DType],
500 params: &OpParams,
501) -> HashMap<String, f64> {
502 let mut b = HashMap::new();
503 if let Some(out) = dtypes.last() {
504 b.insert("dtype_bytes".to_string(), out.size_in_bytes() as f64);
505 }
506 match params {
507 OpParams::Matmul {
508 lhs_batch_dims,
509 m,
510 n,
511 k,
512 ..
513 } => {
514 let batch = lhs_batch_dims.iter().copied().product::<usize>().max(1);
515 b.insert("batch".to_string(), batch as f64);
516 b.insert("m".to_string(), *m as f64);
517 b.insert("n".to_string(), *n as f64);
518 b.insert("k".to_string(), *k as f64);
519 }
520 _ => {
521 if let Some(out_shape) = shapes.last() {
523 b.insert("n".to_string(), out_shape.elem_count() as f64);
524 }
525 }
526 }
527 b
528}
529
530pub fn cost_estimate(
537 expr: &CompiledCostExpr,
538 op: OpKind,
539 shapes: &[Shape],
540 dtypes: &[DType],
541 params: &OpParams,
542) -> Result<CostEstimate, CostEvalError> {
543 let symbols = bind_cost_symbols(op, shapes, dtypes, params);
544 let flops = eval(expr, &symbols)?;
545 Ok(CostEstimate {
546 flops: flops.max(0.0) as u64,
547 ..CostEstimate::default()
548 })
549}
550
551fn canonical_key(node: &CostNode) -> String {
555 match node {
556 CostNode::Lit(v) => {
557 if v.fract() == 0.0 {
558 format!("{}", *v as i64)
559 } else {
560 format!("{v}")
561 }
562 }
563 CostNode::Sym(s) => s.clone(),
564 CostNode::Index { base, index } => {
565 format!("{}[{}]", canonical_key(base), canonical_key(index))
566 }
567 CostNode::Call { name, args } => {
568 let inner: Vec<String> = args.iter().map(canonical_key).collect();
569 format!("{name}({})", inner.join(","))
570 }
571 CostNode::Neg(inner) => format!("-{}", canonical_key(inner)),
572 CostNode::Bin { .. } => "<expr>".to_string(),
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 fn b(pairs: &[(&str, f64)]) -> HashMap<String, f64> {
581 pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
582 }
583
584 #[test]
585 fn parses_and_evals_3_times_n_times_4() {
586 let expr = compile_field(Some("3 * n * 4")).expect("parses");
588 let v = eval(&expr, &b(&[("n", 8.0)])).expect("evals");
589 assert_eq!(v, 96.0);
590 }
591
592 #[test]
593 fn precedence_mul_binds_tighter_than_add() {
594 let expr = compile_field(Some("2 + 3 * 4")).unwrap();
596 assert_eq!(eval(&expr, &b(&[])).unwrap(), 14.0);
597 }
598
599 #[test]
600 fn parens_override_precedence() {
601 let expr = compile_field(Some("(2 + 3) * 4")).unwrap();
602 assert_eq!(eval(&expr, &b(&[])).unwrap(), 20.0);
603 }
604
605 #[test]
606 fn matmul_flops_2_m_n_k() {
607 let expr = compile_field(Some("2 * m * n * k")).unwrap();
608 let v = eval(&expr, &b(&[("m", 4.0), ("n", 8.0), ("k", 16.0)])).unwrap();
609 assert_eq!(v, 2.0 * 4.0 * 8.0 * 16.0);
610 }
611
612 #[test]
613 fn rem_and_div_and_float_literal() {
614 assert_eq!(
615 eval(&compile_field(Some("10 % 3")).unwrap(), &b(&[])).unwrap(),
616 1.0
617 );
618 assert_eq!(
619 eval(&compile_field(Some("12 / 4")).unwrap(), &b(&[])).unwrap(),
620 3.0
621 );
622 assert_eq!(
623 eval(&compile_field(Some("2.5 * 2")).unwrap(), &b(&[])).unwrap(),
624 5.0
625 );
626 }
627
628 #[test]
629 fn unary_negation() {
630 assert_eq!(
631 eval(&compile_field(Some("-3 + 5")).unwrap(), &b(&[])).unwrap(),
632 2.0
633 );
634 }
635
636 #[test]
637 fn dtype_bytes_symbol() {
638 let expr = compile_field(Some("3 * n * dtype_bytes")).unwrap();
639 let v = eval(&expr, &b(&[("n", 10.0), ("dtype_bytes", 4.0)])).unwrap();
640 assert_eq!(v, 120.0);
641 }
642
643 #[test]
644 fn none_and_empty_are_unknown() {
645 assert_eq!(compile_field(None).unwrap(), CompiledCostExpr::Unknown);
646 assert_eq!(
647 compile_field(Some(" ")).unwrap(),
648 CompiledCostExpr::Unknown
649 );
650 assert_eq!(eval(&CompiledCostExpr::Unknown, &b(&[])).unwrap(), 0.0);
652 }
653
654 #[test]
655 fn malformed_expressions_are_parse_errors() {
656 assert!(compile_field(Some("k_len <= sk garbage(")).is_err());
659 assert!(compile_field(Some("2 *")).is_err());
660 assert!(compile_field(Some("(2 + 3")).is_err());
661 assert!(compile_field(Some("2 2")).is_err());
662 assert!(compile_field(Some("@")).is_err());
663 assert!(compile_field(Some("==")).is_err());
664 }
665
666 #[test]
667 fn undefined_symbol_is_eval_error() {
668 let expr = compile_field(Some("n * missing")).unwrap();
669 assert!(eval(&expr, &b(&[("n", 4.0)])).is_err());
670 }
671
672 #[test]
675 fn shape_index_and_member_path_parse() {
676 compile_field(Some(
678 "2 * out_shape[0] * (x_shape[1] / groups) * w_shape[2] * w_shape[3]",
679 ))
680 .expect("out_shape[i] parses");
681 compile_field(Some(
682 "2 * out_elems * (x.dim[1] / groups) * weight.dim[2] * weight.dim[3]",
683 ))
684 .expect("role.dim[i] parses");
685 compile_field(Some(
686 "2 * x.dim[0] * x.dim[1] * (x.dim[2] - weight.dim[2] + 1) * weight.dim[2]",
687 ))
688 .expect("causal-conv expr parses");
689 }
690
691 #[test]
692 fn function_call_term_parses() {
693 compile_field(Some(
695 "(batch_count*m*k*4 + n*k*block_bytes(quant_type) + batch_count*m*n*4)",
696 ))
697 .expect("block_bytes(quant_type) parses");
698 }
699
700 #[test]
701 fn shape_index_evaluates_from_canonical_binding() {
702 let expr = compile_field(Some("2 * out_shape[0]")).unwrap();
703 let v = eval(&expr, &b(&[("out_shape[0]", 8.0)])).unwrap();
704 assert_eq!(v, 16.0);
705 }
706
707 #[test]
708 fn member_path_is_one_dotted_symbol() {
709 let node = parse_expr("x.dim").unwrap();
711 assert_eq!(node, CostNode::Sym("x.dim".to_string()));
712 }
713
714 #[test]
715 fn unbalanced_bracket_is_parse_error() {
716 assert!(compile_field(Some("out_shape[0")).is_err());
717 assert!(compile_field(Some("x.dim 0]")).is_err());
718 }
719
720 #[test]
721 fn lone_dot_is_still_a_parse_error() {
722 assert!(compile_field(Some("2 . 3")).is_err());
724 }
725}