1use std::sync::OnceLock;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use smallvec::SmallVec;
5
6use crate::{
7 Ident, IdentWithToken, Shared,
8 ast::{
9 Program, TokenId,
10 node::{self as ast, Args, Branches, Literal, MatchArm, MatchArms, Params, Pattern, StringSegment},
11 },
12 selector::Selector,
13};
14
15type LiteralEnv = SmallVec<[(Ident, Literal); 8]>;
17
18fn env_get(env: &LiteralEnv, key: Ident) -> Option<&Literal> {
19 env.iter().rev().find(|(k, _)| *k == key).map(|(_, v)| v)
20}
21
22fn env_insert(env: &mut LiteralEnv, key: Ident, val: Literal) {
23 if let Some(entry) = env.iter_mut().find(|(k, _)| *k == key) {
24 entry.1 = val;
25 } else {
26 env.push((key, val));
27 }
28}
29
30fn env_remove(env: &mut LiteralEnv, key: Ident) {
31 env.retain(|(k, _)| *k != key);
32}
33
34#[inline]
36fn ptr_eq<T: ?Sized>(a: &Shared<T>, b: &Shared<T>) -> bool {
37 #[cfg(not(feature = "sync"))]
38 {
39 std::rc::Rc::ptr_eq(a, b)
40 }
41 #[cfg(feature = "sync")]
42 {
43 std::sync::Arc::ptr_eq(a, b)
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum OptimizationLevel {
54 #[default]
55 None,
56 Basic,
57 Full,
58}
59
60fn lazy_map_program<F>(program: Program, mut f: F) -> Program
66where
67 F: FnMut(&Shared<ast::Node>) -> Shared<ast::Node>,
68{
69 let mut result: Option<Vec<Shared<ast::Node>>> = None;
70 for (i, node) in program.iter().enumerate() {
71 let opt = f(node);
72 if !ptr_eq(&opt, node) {
73 if result.is_none() {
74 let mut r = Vec::with_capacity(program.len());
75 r.extend(program[..i].iter().cloned());
76 result = Some(r);
77 }
78 result.as_mut().unwrap().push(opt);
79 } else if let Some(ref mut r) = result {
80 r.push(Shared::clone(node));
81 }
82 }
83 result.unwrap_or(program)
84}
85
86#[derive(Default)]
88pub struct Optimizer {
89 level: OptimizationLevel,
90}
91
92impl Optimizer {
93 pub fn with_level(level: OptimizationLevel) -> Self {
95 Self { level }
96 }
97
98 pub fn optimize(&self, program: Program) -> Program {
100 self.optimize_impl(program, true)
101 }
102
103 fn optimize_nested(&self, program: Program, parent_user_defs: &FxHashSet<Ident>) -> Program {
105 if matches!(self.level, OptimizationLevel::None) {
106 return program;
107 }
108
109 let merged;
112 let user_defs: &FxHashSet<Ident> = if program.iter().any(|n| matches!(&*n.expr, ast::Expr::Def(..))) {
113 merged = parent_user_defs
114 .iter()
115 .copied()
116 .chain(program.iter().filter_map(|n| {
117 if let ast::Expr::Def(ident, ..) = &*n.expr {
118 Some(ident.name)
119 } else {
120 None
121 }
122 }))
123 .collect();
124 &merged
125 } else {
126 parent_user_defs
127 };
128
129 let optimized = lazy_map_program(program, |n| self.optimize_node(Shared::clone(n), user_defs));
130 self.merge_selector_chains(optimized)
131 }
132
133 fn optimize_impl(&self, program: Program, top_level: bool) -> Program {
135 static EMPTY_DEFS: OnceLock<FxHashSet<Ident>> = OnceLock::new();
139 let user_defs_owned: FxHashSet<Ident>;
140 let user_defs: &FxHashSet<Ident> = if program.iter().any(|n| matches!(&*n.expr, ast::Expr::Def(..))) {
141 user_defs_owned = program
142 .iter()
143 .filter_map(|n| {
144 if let ast::Expr::Def(ident, ..) = &*n.expr {
145 Some(ident.name)
146 } else {
147 None
148 }
149 })
150 .collect();
151 &user_defs_owned
152 } else {
153 EMPTY_DEFS.get_or_init(FxHashSet::default)
154 };
155
156 match self.level {
157 OptimizationLevel::None => program,
158 OptimizationLevel::Basic => {
159 let optimized: Program = program
160 .into_iter()
161 .map(|node| self.optimize_node(node, user_defs))
162 .collect();
163 self.merge_selector_chains(optimized)
164 }
165 OptimizationLevel::Full => {
166 let program = self.propagate_and_fold(program, user_defs);
168 let program = self.merge_selector_chains(program);
169
170 if !program.iter().any(|n| matches!(&*n.expr, ast::Expr::Def(..))) {
172 return program;
173 }
174
175 let inlinable = collect_inlinable(&program);
176 let program: Program = if inlinable.is_empty() {
177 program
178 } else {
179 program.into_iter().map(|n| self.apply_inline(n, &inlinable)).collect()
180 };
181 let program = apply_tco_transforms(program);
182 let program = if top_level {
185 eliminate_dead_defs(program, &inlinable)
186 } else {
187 program
188 };
189 let refolded: Program = program.into_iter().map(|n| self.optimize_node(n, user_defs)).collect();
190 self.merge_selector_chains(refolded)
191 }
192 }
193 }
194
195 fn propagate_and_fold(&self, program: Program, user_defs: &FxHashSet<Ident>) -> Program {
202 let has_let_literal = program.iter().any(|n| {
203 matches!(&*n.expr, ast::Expr::Let(Pattern::Ident(_), rhs) if matches!(&*rhs.expr, ast::Expr::Literal(_)))
204 });
205
206 if !has_let_literal {
207 return lazy_map_program(program, |n| self.optimize_node(Shared::clone(n), user_defs));
208 }
209
210 let mut env: LiteralEnv = LiteralEnv::new();
211 let mut result: Program = Vec::with_capacity(program.len());
212
213 for node in program {
214 let token_id = node.token_id;
215 match &*node.expr {
216 ast::Expr::Let(Pattern::Ident(ident), rhs) => {
217 let opt_rhs = self.optimize_node(Shared::clone(rhs), user_defs);
218 if let ast::Expr::Literal(lit) = &*opt_rhs.expr {
219 env_insert(&mut env, ident.name, lit.clone());
220 } else {
221 env_remove(&mut env, ident.name);
222 }
223 if ptr_eq(&opt_rhs, rhs) {
225 result.push(node);
226 } else {
227 result.push(Shared::new(ast::Node {
228 token_id,
229 expr: Shared::new(ast::Expr::Let(Pattern::Ident(ident.clone()), opt_rhs)),
230 }));
231 }
232 }
233 _ => {
234 let optimized = if env.is_empty() {
235 self.optimize_node(node, user_defs)
236 } else {
237 self.optimize_node(self.substitute_literals(node, &env), user_defs)
238 };
239 result.push(optimized);
240 }
241 }
242 }
243
244 result
245 }
246
247 fn merge_selector_chains(&self, program: Program) -> Program {
248 let has_consecutive = program
250 .windows(2)
251 .any(|w| matches!(&*w[0].expr, ast::Expr::Selector(_)) && matches!(&*w[1].expr, ast::Expr::Selector(_)));
252 if !has_consecutive {
253 return program;
254 }
255
256 let mut result: Program = Vec::with_capacity(program.len());
257 let mut iter = program.into_iter().peekable();
258
259 while let Some(node) = iter.next() {
260 if let ast::Expr::Selector(sel) = &*node.expr {
261 let token_id = node.token_id;
262 let mut chain: SmallVec<[Selector; 4]> = SmallVec::new();
263 chain.push(sel.clone());
264
265 while let Some(next) = iter.peek() {
266 if let ast::Expr::Selector(next_sel) = &*next.expr {
267 chain.push(next_sel.clone());
268 iter.next();
269 } else {
270 break;
271 }
272 }
273
274 if chain.len() == 1 {
275 result.push(node);
276 } else {
277 result.push(Shared::new(ast::Node {
278 token_id,
279 expr: Shared::new(ast::Expr::SelectorChain(chain)),
280 }));
281 }
282 } else {
283 result.push(node);
284 }
285 }
286
287 result
288 }
289
290 fn substitute_literals(&self, node: Shared<ast::Node>, env: &LiteralEnv) -> Shared<ast::Node> {
292 if env.is_empty() {
293 return node;
294 }
295 let token_id = node.token_id;
296
297 match &*node.expr {
298 ast::Expr::Ident(ident) => {
299 if let Some(lit) = env_get(env, ident.name) {
300 return Shared::new(ast::Node {
301 token_id,
302 expr: Shared::new(ast::Expr::Literal(lit.clone())),
303 });
304 }
305 node
306 }
307 ast::Expr::Call(ident, args) => {
308 let subst_args: Args = args
309 .iter()
310 .map(|a| self.substitute_literals(Shared::clone(a), env))
311 .collect();
312 Shared::new(ast::Node {
313 token_id,
314 expr: Shared::new(ast::Expr::Call(ident.clone(), subst_args)),
315 })
316 }
317 ast::Expr::CallDynamic(callable, args) => {
318 let subst_callable = self.substitute_literals(Shared::clone(callable), env);
319 let subst_args: Args = args
320 .iter()
321 .map(|a| self.substitute_literals(Shared::clone(a), env))
322 .collect();
323 Shared::new(ast::Node {
324 token_id,
325 expr: Shared::new(ast::Expr::CallDynamic(subst_callable, subst_args)),
326 })
327 }
328 ast::Expr::SelectorCall(selector, args) => {
329 let subst_args: Args = args
330 .iter()
331 .map(|a| self.substitute_literals(Shared::clone(a), env))
332 .collect();
333 Shared::new(ast::Node {
334 token_id,
335 expr: Shared::new(ast::Expr::SelectorCall(selector.clone(), subst_args)),
336 })
337 }
338 ast::Expr::If(branches) => {
339 let subst_branches: Branches = branches
340 .iter()
341 .map(|(cond, body)| {
342 (
343 cond.as_ref().map(|c| self.substitute_literals(Shared::clone(c), env)),
344 self.substitute_literals(Shared::clone(body), env),
345 )
346 })
347 .collect();
348 Shared::new(ast::Node {
349 token_id,
350 expr: Shared::new(ast::Expr::If(subst_branches)),
351 })
352 }
353 ast::Expr::And(operands) => {
354 let subst: Vec<_> = operands
355 .iter()
356 .map(|o| self.substitute_literals(Shared::clone(o), env))
357 .collect();
358 Shared::new(ast::Node {
359 token_id,
360 expr: Shared::new(ast::Expr::And(subst)),
361 })
362 }
363 ast::Expr::Or(operands) => {
364 let subst: Vec<_> = operands
365 .iter()
366 .map(|o| self.substitute_literals(Shared::clone(o), env))
367 .collect();
368 Shared::new(ast::Node {
369 token_id,
370 expr: Shared::new(ast::Expr::Or(subst)),
371 })
372 }
373 ast::Expr::Paren(inner) => Shared::new(ast::Node {
374 token_id,
375 expr: Shared::new(ast::Expr::Paren(self.substitute_literals(Shared::clone(inner), env))),
376 }),
377 ast::Expr::Try(try_expr, catch_expr) => Shared::new(ast::Node {
378 token_id,
379 expr: Shared::new(ast::Expr::Try(
380 self.substitute_literals(Shared::clone(try_expr), env),
381 self.substitute_literals(Shared::clone(catch_expr), env),
382 )),
383 }),
384 ast::Expr::Break(Some(val)) => Shared::new(ast::Node {
385 token_id,
386 expr: Shared::new(ast::Expr::Break(Some(
387 self.substitute_literals(Shared::clone(val), env),
388 ))),
389 }),
390 ast::Expr::InterpolatedString(segments) => {
393 let subst_segs: Vec<StringSegment> = segments
394 .iter()
395 .map(|seg| match seg {
396 StringSegment::Expr(inner) => {
397 StringSegment::Expr(self.substitute_literals(Shared::clone(inner), env))
398 }
399 other => other.clone(),
400 })
401 .collect();
402 Shared::new(ast::Node {
403 token_id,
404 expr: Shared::new(ast::Expr::InterpolatedString(subst_segs)),
405 })
406 }
407 ast::Expr::Block(_)
409 | ast::Expr::Def(_, _, _)
410 | ast::Expr::Fn(_, _)
411 | ast::Expr::While(_, _)
412 | ast::Expr::Loop(_)
413 | ast::Expr::Foreach(_, _, _)
414 | ast::Expr::Let(_, _)
415 | ast::Expr::Var(_, _)
416 | ast::Expr::As(_, _)
417 | ast::Expr::Assign(_, _)
418 | ast::Expr::Match(_, _)
419 | ast::Expr::Literal(_)
420 | ast::Expr::Selector(_)
421 | ast::Expr::SelectorChain(_)
422 | ast::Expr::Self_
423 | ast::Expr::Nodes
424 | ast::Expr::Break(None)
425 | ast::Expr::Continue
426 | ast::Expr::Include(_)
427 | ast::Expr::Import(_)
428 | ast::Expr::Module(_, _)
429 | ast::Expr::Macro(_, _, _)
430 | ast::Expr::Quote(_)
431 | ast::Expr::Unquote(_)
432 | ast::Expr::QualifiedAccess(_, _) => node,
433 }
434 }
435
436 fn apply_inline(&self, node: Shared<ast::Node>, fns: &FxHashMap<Ident, InlinableFn>) -> Shared<ast::Node> {
437 if fns.is_empty() {
438 return node;
439 }
440 let token_id = node.token_id;
441
442 match &*node.expr {
443 ast::Expr::Call(ident, args) => {
444 let opt_args: Args = args.iter().map(|a| self.apply_inline(Shared::clone(a), fns)).collect();
445
446 if let Some(f) = fns.get(&ident.name).filter(|f| opt_args.len() == f.params.len()) {
447 return substitute_params(Shared::clone(&f.body), &f.params, &opt_args, token_id);
448 }
449
450 Shared::new(ast::Node {
451 token_id,
452 expr: Shared::new(ast::Expr::Call(ident.clone(), opt_args)),
453 })
454 }
455 ast::Expr::If(branches) => {
457 let branches: ast::Branches = branches
458 .iter()
459 .map(|(cond, body)| {
460 (
461 cond.as_ref().map(|c| self.apply_inline(Shared::clone(c), fns)),
462 self.apply_inline(Shared::clone(body), fns),
463 )
464 })
465 .collect();
466 Shared::new(ast::Node {
467 token_id,
468 expr: Shared::new(ast::Expr::If(branches)),
469 })
470 }
471 ast::Expr::And(ops) => Shared::new(ast::Node {
472 token_id,
473 expr: Shared::new(ast::Expr::And(
474 ops.iter().map(|o| self.apply_inline(Shared::clone(o), fns)).collect(),
475 )),
476 }),
477 ast::Expr::Or(ops) => Shared::new(ast::Node {
478 token_id,
479 expr: Shared::new(ast::Expr::Or(
480 ops.iter().map(|o| self.apply_inline(Shared::clone(o), fns)).collect(),
481 )),
482 }),
483 ast::Expr::Try(try_expr, catch_expr) => Shared::new(ast::Node {
484 token_id,
485 expr: Shared::new(ast::Expr::Try(
486 self.apply_inline(Shared::clone(try_expr), fns),
487 self.apply_inline(Shared::clone(catch_expr), fns),
488 )),
489 }),
490 ast::Expr::SelectorCall(sel, args) => {
491 let opt_args: Args = args.iter().map(|a| self.apply_inline(Shared::clone(a), fns)).collect();
492 Shared::new(ast::Node {
493 token_id,
494 expr: Shared::new(ast::Expr::SelectorCall(sel.clone(), opt_args)),
495 })
496 }
497 _ => node,
499 }
500 }
501
502 fn optimize_node(&self, node: Shared<ast::Node>, user_defs: &FxHashSet<Ident>) -> Shared<ast::Node> {
503 let token_id = node.token_id;
504
505 match &*node.expr {
506 ast::Expr::Paren(inner) => self.optimize_node(Shared::clone(inner), user_defs),
507 ast::Expr::Call(ident, args) => {
508 let opt_args: Args = args
509 .iter()
510 .map(|a| self.optimize_node(Shared::clone(a), user_defs))
511 .collect();
512 if let Some(folded) = self.try_fold_call(token_id, &ident.name, &opt_args, user_defs) {
513 return folded;
514 }
515 if args.iter().zip(opt_args.iter()).all(|(orig, opt)| ptr_eq(orig, opt)) {
517 return node;
518 }
519 Shared::new(ast::Node {
520 token_id,
521 expr: Shared::new(ast::Expr::Call(ident.clone(), opt_args)),
522 })
523 }
524 ast::Expr::If(branches) => self.optimize_if(token_id, branches, user_defs),
525 ast::Expr::And(operands) => self.optimize_and(token_id, operands, user_defs),
526 ast::Expr::Or(operands) => self.optimize_or(token_id, operands, user_defs),
527 ast::Expr::Block(program) => {
528 let opt = self.optimize_nested(program.clone(), user_defs);
529 if program.iter().zip(opt.iter()).all(|(a, b)| ptr_eq(a, b)) {
530 return node;
531 }
532 Shared::new(ast::Node {
533 token_id,
534 expr: Shared::new(ast::Expr::Block(opt)),
535 })
536 }
537 ast::Expr::Def(ident, params, program) => Shared::new(ast::Node {
538 token_id,
539 expr: Shared::new(ast::Expr::Def(
540 ident.clone(),
541 self.optimize_params(params, user_defs),
542 self.optimize_nested(program.clone(), user_defs),
543 )),
544 }),
545 ast::Expr::Fn(params, program) => Shared::new(ast::Node {
546 token_id,
547 expr: Shared::new(ast::Expr::Fn(
548 self.optimize_params(params, user_defs),
549 self.optimize_nested(program.clone(), user_defs),
550 )),
551 }),
552 ast::Expr::While(cond, program) => {
553 let opt_cond = self.optimize_node(Shared::clone(cond), user_defs);
554 let opt_body = self.optimize_nested(program.clone(), user_defs);
555 if ptr_eq(&opt_cond, cond) && program.iter().zip(opt_body.iter()).all(|(a, b)| ptr_eq(a, b)) {
556 return node;
557 }
558 Shared::new(ast::Node {
559 token_id,
560 expr: Shared::new(ast::Expr::While(opt_cond, opt_body)),
561 })
562 }
563 ast::Expr::Loop(program) => {
564 let opt = self.optimize_nested(program.clone(), user_defs);
565 if program.iter().zip(opt.iter()).all(|(a, b)| ptr_eq(a, b)) {
566 return node;
567 }
568 Shared::new(ast::Node {
569 token_id,
570 expr: Shared::new(ast::Expr::Loop(opt)),
571 })
572 }
573 ast::Expr::Foreach(ident, values, program) => {
574 let opt_values = self.optimize_node(Shared::clone(values), user_defs);
575 let opt_body = self.optimize_nested(program.clone(), user_defs);
576 if ptr_eq(&opt_values, values) && program.iter().zip(opt_body.iter()).all(|(a, b)| ptr_eq(a, b)) {
577 return node;
578 }
579 Shared::new(ast::Node {
580 token_id,
581 expr: Shared::new(ast::Expr::Foreach(ident.clone(), opt_values, opt_body)),
582 })
583 }
584 ast::Expr::As(ident, inner) => {
585 let opt_inner = self.optimize_node(Shared::clone(inner), user_defs);
586 if ptr_eq(&opt_inner, inner) {
587 return node;
588 }
589 Shared::new(ast::Node {
590 token_id,
591 expr: Shared::new(ast::Expr::As(ident.clone(), opt_inner)),
592 })
593 }
594 ast::Expr::Let(pattern, inner) => {
595 let opt_inner = self.optimize_node(Shared::clone(inner), user_defs);
596 if ptr_eq(&opt_inner, inner) {
597 return node;
598 }
599 Shared::new(ast::Node {
600 token_id,
601 expr: Shared::new(ast::Expr::Let(pattern.clone(), opt_inner)),
602 })
603 }
604 ast::Expr::Var(pattern, inner) => {
605 let opt_inner = self.optimize_node(Shared::clone(inner), user_defs);
606 if ptr_eq(&opt_inner, inner) {
607 return node;
608 }
609 Shared::new(ast::Node {
610 token_id,
611 expr: Shared::new(ast::Expr::Var(pattern.clone(), opt_inner)),
612 })
613 }
614 ast::Expr::Assign(ident, inner) => {
615 let opt_inner = self.optimize_node(Shared::clone(inner), user_defs);
616 if ptr_eq(&opt_inner, inner) {
617 return node;
618 }
619 Shared::new(ast::Node {
620 token_id,
621 expr: Shared::new(ast::Expr::Assign(ident.clone(), opt_inner)),
622 })
623 }
624 ast::Expr::Try(try_expr, catch_expr) => {
625 let opt_try = self.optimize_node(Shared::clone(try_expr), user_defs);
626 let opt_catch = self.optimize_node(Shared::clone(catch_expr), user_defs);
627 if ptr_eq(&opt_try, try_expr) && ptr_eq(&opt_catch, catch_expr) {
628 return node;
629 }
630 Shared::new(ast::Node {
631 token_id,
632 expr: Shared::new(ast::Expr::Try(opt_try, opt_catch)),
633 })
634 }
635 ast::Expr::Break(Some(val)) => {
636 let opt_val = self.optimize_node(Shared::clone(val), user_defs);
637 if ptr_eq(&opt_val, val) {
638 return node;
639 }
640 Shared::new(ast::Node {
641 token_id,
642 expr: Shared::new(ast::Expr::Break(Some(opt_val))),
643 })
644 }
645 ast::Expr::Match(value_node, arms) => {
646 let opt_value = self.optimize_node(Shared::clone(value_node), user_defs);
647 let opt_arms: MatchArms = arms
648 .iter()
649 .map(|arm| MatchArm {
650 pattern: arm.pattern.clone(),
651 guard: arm
652 .guard
653 .as_ref()
654 .map(|g| self.optimize_node(Shared::clone(g), user_defs)),
655 body: self.optimize_node(Shared::clone(&arm.body), user_defs),
656 })
657 .collect();
658 Shared::new(ast::Node {
659 token_id,
660 expr: Shared::new(ast::Expr::Match(opt_value, opt_arms)),
661 })
662 }
663 ast::Expr::CallDynamic(callable, args) => {
664 let opt_callable = self.optimize_node(Shared::clone(callable), user_defs);
665 let opt_args: Args = args
666 .iter()
667 .map(|a| self.optimize_node(Shared::clone(a), user_defs))
668 .collect();
669 if ptr_eq(&opt_callable, callable)
670 && args.iter().zip(opt_args.iter()).all(|(orig, opt)| ptr_eq(orig, opt))
671 {
672 return node;
673 }
674 Shared::new(ast::Node {
675 token_id,
676 expr: Shared::new(ast::Expr::CallDynamic(opt_callable, opt_args)),
677 })
678 }
679 ast::Expr::SelectorCall(selector, args) => {
680 let opt_args: Args = args
681 .iter()
682 .map(|a| self.optimize_node(Shared::clone(a), user_defs))
683 .collect();
684 if args.iter().zip(opt_args.iter()).all(|(orig, opt)| ptr_eq(orig, opt)) {
685 return node;
686 }
687 Shared::new(ast::Node {
688 token_id,
689 expr: Shared::new(ast::Expr::SelectorCall(selector.clone(), opt_args)),
690 })
691 }
692 ast::Expr::InterpolatedString(segments) => {
693 let opt_segs: Vec<StringSegment> = segments
696 .iter()
697 .map(|seg| match seg {
698 StringSegment::Expr(n) => {
699 let opt = self.optimize_node(Shared::clone(n), user_defs);
700 if let ast::Expr::Literal(Literal::String(s)) = &*opt.expr {
701 StringSegment::Text(s.clone())
702 } else {
703 StringSegment::Expr(opt)
704 }
705 }
706 other => other.clone(),
707 })
708 .collect();
709
710 if opt_segs.iter().all(|s| matches!(s, StringSegment::Text(_))) {
711 let folded = opt_segs.iter().fold(String::new(), |mut acc, s| {
712 if let StringSegment::Text(t) = s {
713 acc.push_str(t);
714 }
715 acc
716 });
717 return Shared::new(ast::Node {
718 token_id,
719 expr: Shared::new(ast::Expr::Literal(Literal::String(folded))),
720 });
721 }
722
723 Shared::new(ast::Node {
724 token_id,
725 expr: Shared::new(ast::Expr::InterpolatedString(opt_segs)),
726 })
727 }
728 ast::Expr::Module(ident, program) => Shared::new(ast::Node {
729 token_id,
730 expr: Shared::new(ast::Expr::Module(
731 ident.clone(),
732 self.optimize_nested(program.clone(), user_defs),
733 )),
734 }),
735 ast::Expr::Unquote(inner) => Shared::new(ast::Node {
736 token_id,
737 expr: Shared::new(ast::Expr::Unquote(self.optimize_node(Shared::clone(inner), user_defs))),
738 }),
739 ast::Expr::Literal(_)
740 | ast::Expr::Ident(_)
741 | ast::Expr::Selector(_)
742 | ast::Expr::SelectorChain(_)
743 | ast::Expr::Self_
744 | ast::Expr::Nodes
745 | ast::Expr::Break(None)
746 | ast::Expr::Continue
747 | ast::Expr::Include(_)
748 | ast::Expr::Import(_)
749 | ast::Expr::Macro(_, _, _)
750 | ast::Expr::Quote(_)
751 | ast::Expr::QualifiedAccess(_, _) => node,
752 }
753 }
754
755 fn try_fold_call(
756 &self,
757 token_id: TokenId,
758 name: &crate::Ident,
759 args: &Args,
760 user_defs: &FxHashSet<Ident>,
761 ) -> Option<Shared<ast::Node>> {
762 use crate::ast::constants::builtins;
763
764 if user_defs.contains(name) {
766 return None;
767 }
768
769 let make_lit = |lit: Literal| {
770 Shared::new(ast::Node {
771 token_id,
772 expr: Shared::new(ast::Expr::Literal(lit)),
773 })
774 };
775
776 if args.len() == 2 {
777 let lhs_lit = literal_of(&args[0]);
778 let rhs_lit = literal_of(&args[1]);
779
780 if let (Some(lhs), Some(rhs)) = (lhs_lit.clone(), rhs_lit.clone()) {
781 match name.as_str().as_str() {
782 n @ (builtins::ADD | builtins::SUB | builtins::MUL | builtins::DIV | builtins::MOD) => {
783 match (lhs, rhs) {
784 (Literal::Number(a), Literal::Number(b)) => {
785 if (n == builtins::DIV || n == builtins::MOD) && b.is_zero() {
786 return None;
787 }
788 let result = match n {
789 builtins::ADD => a + b,
790 builtins::SUB => a - b,
791 builtins::MUL => a * b,
792 builtins::DIV => a / b,
793 builtins::MOD => a % b,
794 _ => unreachable!(),
795 };
796 return Some(make_lit(Literal::Number(result)));
797 }
798 (Literal::String(a), Literal::String(b)) if n == builtins::ADD => {
799 return Some(make_lit(Literal::String(a + &b)));
800 }
801 _ => {}
802 }
803 }
804
805 builtins::EQ => return Some(make_lit(Literal::Bool(literal_eq(lhs, rhs)))),
806 builtins::NE => return Some(make_lit(Literal::Bool(!literal_eq(lhs, rhs)))),
807
808 n @ (builtins::LT | builtins::LTE | builtins::GT | builtins::GTE) => match (lhs, rhs) {
809 (Literal::Number(a), Literal::Number(b)) => {
810 if a.is_nan() || b.is_nan() {
811 return None;
812 }
813 let result = match n {
814 builtins::LT => a < b,
815 builtins::LTE => a <= b,
816 builtins::GT => a > b,
817 builtins::GTE => a >= b,
818 _ => unreachable!(),
819 };
820 return Some(make_lit(Literal::Bool(result)));
821 }
822 (Literal::String(a), Literal::String(b)) => {
823 let result = match n {
824 builtins::LT => a < b,
825 builtins::LTE => a <= b,
826 builtins::GT => a > b,
827 builtins::GTE => a >= b,
828 _ => unreachable!(),
829 };
830 return Some(make_lit(Literal::Bool(result)));
831 }
832 _ => {}
833 },
834
835 builtins::STARTS_WITH => {
836 if let (Literal::String(s), Literal::String(prefix)) = (lhs, rhs) {
837 return Some(make_lit(Literal::Bool(s.starts_with(&*prefix))));
838 }
839 }
840
841 builtins::ENDS_WITH => {
842 if let (Literal::String(s), Literal::String(suffix)) = (lhs, rhs) {
843 return Some(make_lit(Literal::Bool(s.ends_with(&*suffix))));
844 }
845 }
846
847 builtins::INDEX => {
848 if let (Literal::String(s), Literal::String(sub)) = (lhs, rhs) {
849 let pos = s.find(&*sub).map(|v| v as i64).unwrap_or(-1);
850 return Some(make_lit(Literal::Number(pos.into())));
851 }
852 }
853
854 builtins::RINDEX => {
855 if let (Literal::String(s), Literal::String(sub)) = (lhs, rhs) {
856 let pos = s.rfind(&*sub).map(|v| v as i64).unwrap_or(-1);
857 return Some(make_lit(Literal::Number(pos.into())));
858 }
859 }
860
861 builtins::COALESCE => {
862 return Some(match lhs {
863 Literal::None => Shared::clone(&args[1]),
864 _ => Shared::clone(&args[0]),
865 });
866 }
867
868 _ => {}
869 }
870 }
871
872 if name.as_str().as_str() == builtins::COALESCE {
874 if matches!(&lhs_lit, Some(Literal::None)) {
875 return Some(Shared::clone(&args[1]));
876 }
877 if lhs_lit.as_ref().is_some_and(|lit| !matches!(lit, Literal::None)) {
878 return Some(Shared::clone(&args[0]));
879 }
880 }
881
882 let op = name.as_str();
884 match op.as_str() {
885 builtins::ADD => {
886 if matches!(&rhs_lit, Some(Literal::Number(n)) if n.is_zero()) {
888 return Some(Shared::clone(&args[0]));
889 }
890 if matches!(&lhs_lit, Some(Literal::Number(n)) if n.is_zero()) {
891 return Some(Shared::clone(&args[1]));
892 }
893 if matches!(&rhs_lit, Some(Literal::String(s)) if s.is_empty()) {
895 return Some(Shared::clone(&args[0]));
896 }
897 if matches!(&lhs_lit, Some(Literal::String(s)) if s.is_empty()) {
898 return Some(Shared::clone(&args[1]));
899 }
900 }
901 builtins::SUB => {
902 if matches!(&rhs_lit, Some(Literal::Number(n)) if n.is_zero()) {
904 return Some(Shared::clone(&args[0]));
905 }
906 }
907 builtins::MUL => {
908 if matches!(&rhs_lit, Some(Literal::Number(n)) if is_one(n)) {
910 return Some(Shared::clone(&args[0]));
911 }
912 if matches!(&lhs_lit, Some(Literal::Number(n)) if is_one(n)) {
913 return Some(Shared::clone(&args[1]));
914 }
915 if matches!(&rhs_lit, Some(Literal::Number(n)) if n.is_zero()) {
917 return Some(make_lit(Literal::Number(0i64.into())));
918 }
919 if matches!(&lhs_lit, Some(Literal::Number(n)) if n.is_zero()) {
920 return Some(make_lit(Literal::Number(0i64.into())));
921 }
922 }
923 builtins::DIV => {
924 if matches!(&rhs_lit, Some(Literal::Number(n)) if is_one(n)) {
926 return Some(Shared::clone(&args[0]));
927 }
928 }
929 _ => {}
930 }
931 }
932
933 if args.len() == 1 {
934 let arg = literal_of(&args[0])?;
935 match name.as_str().as_str() {
936 builtins::NOT => {
937 if let Literal::Bool(b) = arg {
938 return Some(make_lit(Literal::Bool(!b)));
939 }
940 }
941 builtins::NEGATE => {
942 if let Literal::Number(n) = arg {
943 return Some(make_lit(Literal::Number(-n)));
944 }
945 }
946 n @ (builtins::FLOOR | builtins::CEIL | builtins::ROUND | builtins::ABS | builtins::TRUNC) => {
948 if let Literal::Number(num) = arg {
949 if num.is_nan() {
950 return None;
951 }
952 let result = match n {
953 builtins::FLOOR => num.value().floor(),
954 builtins::CEIL => num.value().ceil(),
955 builtins::ROUND => num.value().round(),
956 builtins::ABS => num.value().abs(),
957 builtins::TRUNC => num.value().trunc(),
958 _ => unreachable!(),
959 };
960 return Some(make_lit(Literal::Number(result.into())));
961 }
962 }
963 builtins::LEN => match arg {
964 Literal::String(s) => return Some(make_lit(Literal::Number(s.chars().count().into()))),
965 Literal::Bytes(b) => return Some(make_lit(Literal::Number(b.len().into()))),
966 _ => {}
967 },
968 builtins::TO_STRING => {
970 let s = match arg {
971 Literal::String(s) => s,
972 Literal::Number(n) => n.to_string(),
973 Literal::Bool(b) => b.to_string(),
974 Literal::None => String::new(),
975 Literal::Symbol(sym) => sym.to_string(),
976 Literal::Bytes(_) => return None, };
978 return Some(make_lit(Literal::String(s)));
979 }
980 builtins::TO_NUMBER => {
982 if let Literal::String(s) = arg {
983 return s.parse::<f64>().ok().map(|n| make_lit(Literal::Number(n.into())));
984 }
985 }
986 n @ (builtins::TRIM | builtins::LTRIM | builtins::RTRIM) => {
987 if let Literal::String(s) = arg {
988 let result = match n {
989 builtins::TRIM => s.trim().to_string(),
990 builtins::LTRIM => s.trim_start().to_string(),
991 builtins::RTRIM => s.trim_end().to_string(),
992 _ => unreachable!(),
993 };
994 return Some(make_lit(Literal::String(result)));
995 }
996 }
997 n @ (builtins::UPCASE | builtins::DOWNCASE) => {
998 if let Literal::String(s) = arg {
999 let result = match n {
1000 builtins::UPCASE => s.to_uppercase(),
1001 builtins::DOWNCASE => s.to_lowercase(),
1002 _ => unreachable!(),
1003 };
1004 return Some(make_lit(Literal::String(result)));
1005 }
1006 }
1007
1008 _ => {}
1009 }
1010 }
1011
1012 if args.len() == 3 {
1013 let a = literal_of(&args[0]);
1014 let b = literal_of(&args[1]);
1015 let c = literal_of(&args[2]);
1016 if let (Some(Literal::String(s)), Some(Literal::String(from)), Some(Literal::String(to))) = (a, b, c) {
1017 if name.as_str().as_str() == builtins::REPLACE {
1019 return Some(make_lit(Literal::String(s.replace(&*from, &to))));
1020 }
1021 }
1022 }
1023
1024 None
1025 }
1026
1027 fn optimize_if(&self, token_id: TokenId, branches: &Branches, user_defs: &FxHashSet<Ident>) -> Shared<ast::Node> {
1028 let mut remaining: Branches = SmallVec::new();
1029
1030 for (cond_node, body_node) in branches {
1031 let opt_body = self.optimize_node(Shared::clone(body_node), user_defs);
1032
1033 match cond_node {
1034 None => {
1035 remaining.push((None, opt_body));
1036 break;
1037 }
1038 Some(cond) => {
1039 let opt_cond = self.optimize_node(Shared::clone(cond), user_defs);
1040 match &*opt_cond.expr {
1041 ast::Expr::Literal(Literal::Bool(true)) => {
1042 remaining.push((None, opt_body));
1043 break;
1044 }
1045 ast::Expr::Literal(Literal::Bool(false)) => {
1046 continue;
1047 }
1048 _ => {
1049 remaining.push((Some(opt_cond), opt_body));
1050 }
1051 }
1052 }
1053 }
1054 }
1055
1056 match remaining.len() {
1057 0 => Shared::new(ast::Node {
1058 token_id,
1059 expr: Shared::new(ast::Expr::Literal(Literal::None)),
1060 }),
1061 1 if remaining[0].0.is_none() => Shared::clone(&remaining[0].1),
1062 _ => Shared::new(ast::Node {
1063 token_id,
1064 expr: Shared::new(ast::Expr::If(remaining)),
1065 }),
1066 }
1067 }
1068
1069 fn optimize_and(
1070 &self,
1071 token_id: TokenId,
1072 operands: &[Shared<ast::Node>],
1073 user_defs: &FxHashSet<Ident>,
1074 ) -> Shared<ast::Node> {
1075 let mut remaining: Vec<Shared<ast::Node>> = Vec::with_capacity(operands.len());
1076
1077 for op in operands {
1078 let opt = self.optimize_node(Shared::clone(op), user_defs);
1079 match &*opt.expr {
1080 ast::Expr::Literal(lit) if !literal_is_truthy(lit) => {
1081 return Shared::new(ast::Node {
1082 token_id,
1083 expr: Shared::new(ast::Expr::Literal(Literal::Bool(false))),
1084 });
1085 }
1086 ast::Expr::Literal(lit) if literal_is_truthy(lit) => continue,
1087 _ => remaining.push(opt),
1088 }
1089 }
1090
1091 match remaining.len() {
1092 0 => Shared::new(ast::Node {
1093 token_id,
1094 expr: Shared::new(ast::Expr::Literal(Literal::Bool(true))),
1095 }),
1096 _ => Shared::new(ast::Node {
1097 token_id,
1098 expr: Shared::new(ast::Expr::And(remaining)),
1099 }),
1100 }
1101 }
1102
1103 fn optimize_or(
1104 &self,
1105 token_id: TokenId,
1106 operands: &[Shared<ast::Node>],
1107 user_defs: &FxHashSet<Ident>,
1108 ) -> Shared<ast::Node> {
1109 let mut remaining: Vec<Shared<ast::Node>> = Vec::with_capacity(operands.len());
1110
1111 for op in operands {
1112 let opt = self.optimize_node(Shared::clone(op), user_defs);
1113 match &*opt.expr {
1114 ast::Expr::Literal(lit) if literal_is_truthy(lit) => return opt,
1115 ast::Expr::Literal(lit) if !literal_is_truthy(lit) => continue,
1116 _ => remaining.push(opt),
1117 }
1118 }
1119
1120 match remaining.len() {
1121 0 => Shared::new(ast::Node {
1122 token_id,
1123 expr: Shared::new(ast::Expr::Literal(Literal::Bool(false))),
1124 }),
1125 _ => Shared::new(ast::Node {
1126 token_id,
1127 expr: Shared::new(ast::Expr::Or(remaining)),
1128 }),
1129 }
1130 }
1131
1132 fn optimize_params(&self, params: &Params, user_defs: &FxHashSet<Ident>) -> Params {
1133 params
1134 .iter()
1135 .map(|p| ast::Param {
1136 ident: p.ident.clone(),
1137 default: p
1138 .default
1139 .as_ref()
1140 .map(|d| self.optimize_node(Shared::clone(d), user_defs)),
1141 is_variadic: p.is_variadic,
1142 })
1143 .collect()
1144 }
1145}
1146
1147struct InlinableFn {
1148 params: Vec<Ident>,
1149 body: Shared<ast::Node>,
1150}
1151
1152fn collect_inlinable(program: &Program) -> FxHashMap<Ident, InlinableFn> {
1160 let mut map = FxHashMap::default();
1161 for node in program {
1162 let ast::Expr::Def(ident, params, body) = &*node.expr else {
1163 continue;
1164 };
1165 if body.len() != 1 {
1166 continue;
1167 }
1168 if params.iter().any(|p| p.is_variadic || p.default.is_some()) {
1169 continue;
1170 }
1171 let body_node = &body[0];
1172 let param_names: Vec<Ident> = params.iter().map(|p| p.ident.name).collect();
1173 if has_recursion(body_node, ident.name) || has_free_vars(body_node, ¶m_names) {
1174 continue;
1175 }
1176 map.insert(
1177 ident.name,
1178 InlinableFn {
1179 params: param_names,
1180 body: Shared::clone(body_node),
1181 },
1182 );
1183 }
1184 map
1185}
1186
1187fn has_recursion(node: &Shared<ast::Node>, fn_name: Ident) -> bool {
1189 match &*node.expr {
1190 ast::Expr::Call(ident, args) => ident.name == fn_name || args.iter().any(|a| has_recursion(a, fn_name)),
1191 ast::Expr::Ident(ident) => ident.name == fn_name,
1192 ast::Expr::And(ops) | ast::Expr::Or(ops) => ops.iter().any(|o| has_recursion(o, fn_name)),
1193 ast::Expr::If(branches) => branches.iter().any(|(cond, body)| {
1194 cond.as_ref().is_some_and(|c| has_recursion(c, fn_name)) || has_recursion(body, fn_name)
1195 }),
1196 ast::Expr::Try(t, c) => has_recursion(t, fn_name) || has_recursion(c, fn_name),
1197 ast::Expr::SelectorCall(_, args) => args.iter().any(|a| has_recursion(a, fn_name)),
1198 ast::Expr::Paren(inner) => has_recursion(inner, fn_name),
1199 _ => false,
1200 }
1201}
1202
1203fn has_free_vars(node: &Shared<ast::Node>, params: &[Ident]) -> bool {
1210 match &*node.expr {
1211 ast::Expr::Ident(ident) => !params.contains(&ident.name),
1212 ast::Expr::Literal(_) | ast::Expr::Self_ | ast::Expr::Selector(_) | ast::Expr::SelectorChain(_) => false,
1213 ast::Expr::Call(callee, args) => {
1214 params.contains(&callee.name) || args.iter().any(|a| has_free_vars(a, params))
1216 }
1217 ast::Expr::SelectorCall(_, args) => args.iter().any(|a| has_free_vars(a, params)),
1218 ast::Expr::And(ops) | ast::Expr::Or(ops) => ops.iter().any(|o| has_free_vars(o, params)),
1219 ast::Expr::If(branches) => branches
1220 .iter()
1221 .any(|(cond, body)| cond.as_ref().is_some_and(|c| has_free_vars(c, params)) || has_free_vars(body, params)),
1222 ast::Expr::Try(t, c) => has_free_vars(t, params) || has_free_vars(c, params),
1223 ast::Expr::Paren(inner) => has_free_vars(inner, params),
1224 _ => true,
1225 }
1226}
1227
1228fn substitute_params(
1233 node: Shared<ast::Node>,
1234 params: &[Ident],
1235 args: &Args,
1236 call_token_id: TokenId,
1237) -> Shared<ast::Node> {
1238 let token_id = call_token_id;
1239 match &*node.expr {
1240 ast::Expr::Ident(ident) => {
1241 if let Some(pos) = params.iter().position(|p| *p == ident.name) {
1242 return Shared::clone(&args[pos]);
1243 }
1244 node
1245 }
1246 ast::Expr::Call(ident, call_args) => {
1247 let subst: Args = call_args
1248 .iter()
1249 .map(|a| substitute_params(Shared::clone(a), params, args, call_token_id))
1250 .collect();
1251 Shared::new(ast::Node {
1252 token_id,
1253 expr: Shared::new(ast::Expr::Call(ident.clone(), subst)),
1254 })
1255 }
1256 ast::Expr::SelectorCall(sel, call_args) => {
1257 let subst: Args = call_args
1258 .iter()
1259 .map(|a| substitute_params(Shared::clone(a), params, args, call_token_id))
1260 .collect();
1261 Shared::new(ast::Node {
1262 token_id,
1263 expr: Shared::new(ast::Expr::SelectorCall(sel.clone(), subst)),
1264 })
1265 }
1266 ast::Expr::And(ops) => Shared::new(ast::Node {
1267 token_id,
1268 expr: Shared::new(ast::Expr::And(
1269 ops.iter()
1270 .map(|o| substitute_params(Shared::clone(o), params, args, call_token_id))
1271 .collect(),
1272 )),
1273 }),
1274 ast::Expr::Or(ops) => Shared::new(ast::Node {
1275 token_id,
1276 expr: Shared::new(ast::Expr::Or(
1277 ops.iter()
1278 .map(|o| substitute_params(Shared::clone(o), params, args, call_token_id))
1279 .collect(),
1280 )),
1281 }),
1282 ast::Expr::If(branches) => {
1283 let branches: ast::Branches = branches
1284 .iter()
1285 .map(|(cond, body)| {
1286 (
1287 cond.as_ref()
1288 .map(|c| substitute_params(Shared::clone(c), params, args, call_token_id)),
1289 substitute_params(Shared::clone(body), params, args, call_token_id),
1290 )
1291 })
1292 .collect();
1293 Shared::new(ast::Node {
1294 token_id,
1295 expr: Shared::new(ast::Expr::If(branches)),
1296 })
1297 }
1298 ast::Expr::Try(t, c) => Shared::new(ast::Node {
1299 token_id,
1300 expr: Shared::new(ast::Expr::Try(
1301 substitute_params(Shared::clone(t), params, args, call_token_id),
1302 substitute_params(Shared::clone(c), params, args, call_token_id),
1303 )),
1304 }),
1305 ast::Expr::Paren(inner) => substitute_params(Shared::clone(inner), params, args, call_token_id),
1306 _ => node,
1307 }
1308}
1309
1310fn literal_of(node: &Shared<ast::Node>) -> Option<Literal> {
1311 match &*node.expr {
1312 ast::Expr::Literal(lit) => Some(lit.clone()),
1313 _ => None,
1314 }
1315}
1316
1317fn literal_eq(a: Literal, b: Literal) -> bool {
1318 match (a, b) {
1319 (Literal::Number(x), Literal::Number(y)) => x == y,
1320 (Literal::Bool(x), Literal::Bool(y)) => x == y,
1321 (Literal::String(x), Literal::String(y)) => x == y,
1322 (Literal::Symbol(x), Literal::Symbol(y)) => x == y,
1323 (Literal::Bytes(x), Literal::Bytes(y)) => x == y,
1324 (Literal::None, Literal::None) => true,
1325 _ => false,
1326 }
1327}
1328
1329fn apply_tco_transforms(program: Program) -> Program {
1331 program
1332 .into_iter()
1333 .map(|node| {
1334 let ast::Expr::Def(ident, params, body) = &*node.expr else {
1335 return node;
1336 };
1337 let param_names: Vec<Ident> = params.iter().map(|p| p.ident.name).collect();
1338 match try_tco_transform(ident.name, ¶m_names, body, node.token_id) {
1339 Some(new_body) => Shared::new(ast::Node {
1340 token_id: node.token_id,
1341 expr: Shared::new(ast::Expr::Def(ident.clone(), params.clone(), new_body)),
1342 }),
1343 None => node,
1344 }
1345 })
1346 .collect()
1347}
1348
1349fn try_tco_transform(fn_name: Ident, param_names: &[Ident], body: &Program, token_id: TokenId) -> Option<Program> {
1353 if body.len() != 1 {
1354 return None;
1355 }
1356 let ast::Expr::If(branches) = &*body[0].expr else {
1357 return None;
1358 };
1359
1360 let mut has_recursive = false;
1361 let mut has_base = false;
1362
1363 for (_, branch_body) in branches {
1364 if is_direct_self_call(branch_body, fn_name) {
1365 has_recursive = true;
1366 } else if !contains_self_call(branch_body, fn_name) {
1367 has_base = true;
1368 } else {
1369 return None;
1370 }
1371 }
1372
1373 if !has_recursive || !has_base {
1374 return None;
1375 }
1376
1377 Some(build_tco_loop(fn_name, param_names, branches, token_id))
1378}
1379
1380fn is_direct_self_call(node: &Shared<ast::Node>, fn_name: Ident) -> bool {
1382 matches!(&*node.expr, ast::Expr::Call(ident, _) if ident.name == fn_name)
1383}
1384
1385fn contains_self_call(node: &Shared<ast::Node>, fn_name: Ident) -> bool {
1387 match &*node.expr {
1388 ast::Expr::Call(ident, args) => ident.name == fn_name || args.iter().any(|a| contains_self_call(a, fn_name)),
1389 ast::Expr::Ident(ident) => ident.name == fn_name,
1390 ast::Expr::And(ops) | ast::Expr::Or(ops) => ops.iter().any(|o| contains_self_call(o, fn_name)),
1391 ast::Expr::If(branches) => branches.iter().any(|(cond, body)| {
1392 cond.as_ref().is_some_and(|c| contains_self_call(c, fn_name)) || contains_self_call(body, fn_name)
1393 }),
1394 ast::Expr::Try(t, c) => contains_self_call(t, fn_name) || contains_self_call(c, fn_name),
1395 ast::Expr::SelectorCall(_, args) => args.iter().any(|a| contains_self_call(a, fn_name)),
1396 ast::Expr::Paren(inner) | ast::Expr::Break(Some(inner)) | ast::Expr::Unquote(inner) => {
1397 contains_self_call(inner, fn_name)
1398 }
1399 ast::Expr::Block(prog) => prog.iter().any(|n| contains_self_call(n, fn_name)),
1400 _ => false,
1401 }
1402}
1403
1404fn build_tco_loop(fn_name: Ident, param_names: &[Ident], branches: &Branches, token_id: TokenId) -> Program {
1418 let syn = |expr: ast::Expr| -> Shared<ast::Node> {
1419 Shared::new(ast::Node {
1420 token_id,
1421 expr: Shared::new(expr),
1422 })
1423 };
1424
1425 let tco_ident = |p: Ident| IdentWithToken::new(&format!("__tco_{}", p.as_str()));
1426
1427 let var_decls: Program = param_names
1429 .iter()
1430 .map(|p| {
1431 syn(ast::Expr::Var(
1432 Pattern::Ident(tco_ident(*p)),
1433 syn(ast::Expr::Ident(IdentWithToken::new(&p.as_str()))),
1434 ))
1435 })
1436 .collect();
1437
1438 let let_rebinds: Program = param_names
1440 .iter()
1441 .map(|p| {
1442 syn(ast::Expr::Let(
1443 Pattern::Ident(IdentWithToken::new(&p.as_str())),
1444 syn(ast::Expr::Ident(tco_ident(*p))),
1445 ))
1446 })
1447 .collect();
1448
1449 let new_branches: Branches = branches
1451 .iter()
1452 .map(|(cond, body)| {
1453 let new_body = if is_direct_self_call(body, fn_name) {
1454 let ast::Expr::Call(_, rec_args) = &*body.expr else {
1455 unreachable!()
1456 };
1457 let mut block: Program = param_names
1459 .iter()
1460 .zip(rec_args.iter())
1461 .map(|(p, new_val)| syn(ast::Expr::Assign(tco_ident(*p), Shared::clone(new_val))))
1462 .collect();
1463 block.push(syn(ast::Expr::Continue));
1464 syn(ast::Expr::Block(block))
1465 } else {
1466 syn(ast::Expr::Break(Some(Shared::clone(body))))
1467 };
1468 (cond.clone(), new_body)
1469 })
1470 .collect();
1471
1472 let mut loop_body = let_rebinds;
1473 loop_body.push(syn(ast::Expr::If(new_branches)));
1474
1475 let mut result = var_decls;
1476 result.push(syn(ast::Expr::Loop(loop_body)));
1477 result
1478}
1479
1480#[inline]
1482fn is_one(n: &crate::number::Number) -> bool {
1483 (n.value() - 1.0).abs() < f64::EPSILON
1484}
1485
1486fn collect_called_fns(program: &Program) -> FxHashSet<Ident> {
1488 let mut set = FxHashSet::default();
1489 for node in program {
1490 collect_called_fns_node(node, &mut set);
1491 }
1492 set
1493}
1494
1495fn collect_called_fns_node(node: &Shared<ast::Node>, set: &mut FxHashSet<Ident>) {
1496 match &*node.expr {
1497 ast::Expr::Call(ident, args) => {
1498 set.insert(ident.name);
1499 for a in args {
1500 collect_called_fns_node(a, set);
1501 }
1502 }
1503 ast::Expr::Ident(ident) => {
1506 set.insert(ident.name);
1507 }
1508 ast::Expr::Def(_, _, body) | ast::Expr::Block(body) | ast::Expr::Loop(body) | ast::Expr::Module(_, body) => {
1509 for n in body {
1510 collect_called_fns_node(n, set);
1511 }
1512 }
1513 ast::Expr::Fn(_, body) => {
1514 for n in body {
1515 collect_called_fns_node(n, set);
1516 }
1517 }
1518 ast::Expr::If(branches) => {
1519 for (cond, body) in branches {
1520 if let Some(c) = cond {
1521 collect_called_fns_node(c, set);
1522 }
1523 collect_called_fns_node(body, set);
1524 }
1525 }
1526 ast::Expr::And(ops) | ast::Expr::Or(ops) => {
1527 for o in ops {
1528 collect_called_fns_node(o, set);
1529 }
1530 }
1531 ast::Expr::Try(t, c) => {
1532 collect_called_fns_node(t, set);
1533 collect_called_fns_node(c, set);
1534 }
1535 ast::Expr::SelectorCall(_, args) => {
1536 for a in args {
1537 collect_called_fns_node(a, set);
1538 }
1539 }
1540 ast::Expr::CallDynamic(callee, args) => {
1541 collect_called_fns_node(callee, set);
1542 for a in args {
1543 collect_called_fns_node(a, set);
1544 }
1545 }
1546 ast::Expr::Let(_, rhs) | ast::Expr::Var(_, rhs) | ast::Expr::Assign(_, rhs) | ast::Expr::As(_, rhs) => {
1547 collect_called_fns_node(rhs, set);
1548 }
1549 ast::Expr::While(cond, body) | ast::Expr::Foreach(_, cond, body) => {
1550 collect_called_fns_node(cond, set);
1551 for n in body {
1552 collect_called_fns_node(n, set);
1553 }
1554 }
1555 ast::Expr::Match(val, arms) => {
1556 collect_called_fns_node(val, set);
1557 for arm in arms {
1558 if let Some(g) = &arm.guard {
1559 collect_called_fns_node(g, set);
1560 }
1561 collect_called_fns_node(&arm.body, set);
1562 }
1563 }
1564 ast::Expr::Paren(inner) | ast::Expr::Break(Some(inner)) | ast::Expr::Unquote(inner) => {
1565 collect_called_fns_node(inner, set);
1566 }
1567 ast::Expr::InterpolatedString(segs) => {
1568 for seg in segs {
1569 if let StringSegment::Expr(n) = seg {
1570 collect_called_fns_node(n, set);
1571 }
1572 }
1573 }
1574 _ => {}
1575 }
1576}
1577
1578fn eliminate_dead_defs(program: Program, inlinable: &FxHashMap<Ident, InlinableFn>) -> Program {
1584 if inlinable.is_empty() {
1585 return program;
1586 }
1587 let used = collect_called_fns(&program);
1588 program
1589 .into_iter()
1590 .filter(|node| match &*node.expr {
1591 ast::Expr::Def(ident, _, _) => !inlinable.contains_key(&ident.name) || used.contains(&ident.name),
1592 _ => true,
1593 })
1594 .collect()
1595}
1596
1597fn literal_is_truthy(lit: &Literal) -> bool {
1598 match lit {
1599 Literal::Bool(b) => *b,
1600 Literal::Number(n) => !n.is_zero(),
1601 Literal::String(s) => !s.is_empty(),
1602 Literal::Symbol(_) => true,
1603 Literal::Bytes(b) => !b.is_empty(),
1604 Literal::None => false,
1605 }
1606}
1607
1608#[cfg(test)]
1609mod tests {
1610 use crate::{
1611 DefaultEngine,
1612 ast::node::{Expr, Literal},
1613 optimizer::OptimizationLevel,
1614 };
1615 use rstest::rstest;
1616
1617 fn ast_with(query: &str, level: OptimizationLevel) -> crate::ast::Program {
1619 let mut engine = DefaultEngine::default();
1620 engine.set_optimization_level(level);
1621 engine.compile(query).unwrap().program().clone()
1622 }
1623
1624 fn ast_none(query: &str) -> crate::ast::Program {
1625 ast_with(query, OptimizationLevel::None)
1626 }
1627
1628 fn ast_basic(query: &str) -> crate::ast::Program {
1629 ast_with(query, OptimizationLevel::Basic)
1630 }
1631
1632 fn ast_full(query: &str) -> crate::ast::Program {
1633 ast_with(query, OptimizationLevel::Full)
1634 }
1635
1636 fn assert_literal(node: &crate::Shared<crate::AstNode>, expected: &str, ctx: &str) {
1637 match &*node.expr {
1638 Expr::Literal(lit) => assert_eq!(lit.to_string(), expected, "{ctx}"),
1639 other => panic!("{ctx}: expected Literal({expected:?}), got {other:?}"),
1640 }
1641 }
1642
1643 #[test]
1644 fn none_arithmetic_stays_as_call() {
1645 let prog = ast_none("1 + 2");
1646 assert_eq!(prog.len(), 1);
1647 assert!(
1648 matches!(&*prog[0].expr, Expr::Call(..)),
1649 "None: expected Call, got {:?}",
1650 prog[0].expr
1651 );
1652 }
1653
1654 #[test]
1655 fn none_consecutive_selectors_stay_separate() {
1656 let prog = ast_none(".h1 | .text");
1657 assert_eq!(prog.len(), 2, "None must not merge selectors");
1658 assert!(matches!(&*prog[0].expr, Expr::Selector(_)));
1659 assert!(matches!(&*prog[1].expr, Expr::Selector(_)));
1660 }
1661
1662 #[test]
1663 fn none_if_with_literal_cond_stays_as_if() {
1664 let prog = ast_none("if (true): 1 else: 2");
1665 assert_eq!(prog.len(), 1);
1666 assert!(
1667 matches!(&*prog[0].expr, Expr::If(_)),
1668 "None: expected If, got {:?}",
1669 prog[0].expr
1670 );
1671 }
1672
1673 #[test]
1674 fn none_and_stays_as_and() {
1675 let prog = ast_none("false && .");
1676 assert_eq!(prog.len(), 1);
1677 assert!(
1678 matches!(&*prog[0].expr, Expr::And(_)),
1679 "None: expected And, got {:?}",
1680 prog[0].expr
1681 );
1682 }
1683
1684 #[test]
1685 fn none_interpolated_string_stays_unfolded() {
1686 let prog = ast_none("s\"hello world\"");
1687 assert_eq!(prog.len(), 1);
1688 assert!(
1689 matches!(&*prog[0].expr, Expr::InterpolatedString(_)),
1690 "None: expected InterpolatedString, got {:?}",
1691 prog[0].expr
1692 );
1693 }
1694
1695 #[test]
1696 fn none_def_body_stays_as_if_no_tco() {
1697 let prog = ast_none("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);");
1698 assert_eq!(prog.len(), 1);
1699 let Expr::Def(_, _, body) = &*prog[0].expr else {
1700 panic!("expected Def");
1701 };
1702 assert!(
1703 !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))),
1704 "None: must not apply TCO; Loop found in body"
1705 );
1706 assert!(
1707 body.iter().any(|n| matches!(&*n.expr, Expr::If(_))),
1708 "None: original If must remain in body"
1709 );
1710 }
1711
1712 #[rstest]
1713 #[case("1 + 2", "3")]
1714 #[case("10 - 3", "7")]
1715 #[case("3 * 4", "12")]
1716 #[case("10 / 2", "5")]
1717 #[case("10 % 3", "1")]
1718 #[case("\"hello\" + \" world\"", "hello world")]
1719 #[case("negate(5)", "-5")]
1720 #[case("not(false)", "true")]
1721 #[case("not(true)", "false")]
1722 fn fold_arithmetic_to_literal(#[case] query: &str, #[case] expected: &str) {
1723 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1724 let prog = ast_with(query, level);
1725 assert_eq!(prog.len(), 1, "{level:?}: expected single literal node");
1726 assert_literal(&prog[0], expected, &format!("{level:?}"));
1727 }
1728 }
1729
1730 #[rstest]
1731 #[case("1 == 1", "true")]
1732 #[case("1 == 2", "false")]
1733 #[case("1 != 2", "true")]
1734 #[case("1 != 1", "false")]
1735 #[case("1 < 2", "true")]
1736 #[case("2 < 1", "false")]
1737 #[case("2 <= 2", "true")]
1738 #[case("3 > 2", "true")]
1739 #[case("2 > 3", "false")]
1740 #[case("2 >= 2", "true")]
1741 fn fold_comparisons_to_literal(#[case] query: &str, #[case] expected: &str) {
1742 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1743 let prog = ast_with(query, level);
1744 assert_eq!(prog.len(), 1, "{level:?}: expected single literal node");
1745 assert_literal(&prog[0], expected, &format!("{level:?}"));
1746 }
1747 }
1748
1749 #[test]
1750 fn fold_nested_arithmetic_to_literal() {
1751 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1753 let prog = ast_with("(1 + 2) * (3 + 4)", level);
1754 assert_eq!(prog.len(), 1, "{level:?}");
1755 assert_literal(&prog[0], "21", &format!("{level:?}"));
1756 }
1757 }
1758
1759 #[test]
1760 fn fold_double_negation_to_literal() {
1761 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1763 let prog = ast_with("not(not(true))", level);
1764 assert_eq!(prog.len(), 1, "{level:?}");
1765 assert_literal(&prog[0], "true", &format!("{level:?}"));
1766 }
1767 }
1768
1769 #[test]
1770 fn div_by_zero_not_folded() {
1771 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1773 let prog = ast_with("1 / 0", level);
1774 assert_eq!(prog.len(), 1, "{level:?}");
1775 assert!(
1776 matches!(&*prog[0].expr, Expr::Call(..)),
1777 "{level:?}: div-by-zero must stay as Call, got {:?}",
1778 prog[0].expr
1779 );
1780 }
1781 }
1782
1783 #[test]
1784 fn dynamic_arg_prevents_folding() {
1785 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1787 let prog = ast_with("add(., 1)", level);
1788 assert_eq!(prog.len(), 1, "{level:?}");
1789 assert!(
1790 matches!(&*prog[0].expr, Expr::Call(..)),
1791 "{level:?}: dynamic arg must prevent folding, got {:?}",
1792 prog[0].expr
1793 );
1794 }
1795 }
1796
1797 #[test]
1798 fn if_true_collapses_to_then_body() {
1799 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1800 let prog = ast_with("if (true): 1 else: 2", level);
1801 assert_eq!(prog.len(), 1, "{level:?}");
1802 assert_literal(&prog[0], "1", &format!("{level:?}: if(true)"));
1803 }
1804 }
1805
1806 #[test]
1807 fn if_false_collapses_to_else_body() {
1808 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1809 let prog = ast_with("if (false): 1 else: 2", level);
1810 assert_eq!(prog.len(), 1, "{level:?}");
1811 assert_literal(&prog[0], "2", &format!("{level:?}: if(false)+else"));
1812 }
1813 }
1814
1815 #[test]
1816 fn if_false_no_else_collapses_to_literal_none() {
1817 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1819 let prog = ast_with("if (false): 1", level);
1820 assert_eq!(prog.len(), 1, "{level:?}");
1821 assert!(
1822 matches!(&*prog[0].expr, Expr::Literal(Literal::None)),
1823 "{level:?}: expected Literal(None), got {:?}",
1824 prog[0].expr
1825 );
1826 }
1827 }
1828
1829 #[test]
1830 fn if_foldable_condition_also_eliminated() {
1831 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1833 let prog = ast_with("if (1 == 1): \"yes\" else: \"no\"", level);
1834 assert_eq!(prog.len(), 1, "{level:?}");
1835 assert_literal(&prog[0], "yes", &format!("{level:?}"));
1836 }
1837 }
1838
1839 #[test]
1840 fn if_dynamic_condition_stays_as_if() {
1841 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1842 let prog = ast_with("if (.): 1 else: 2", level);
1843 assert_eq!(prog.len(), 1, "{level:?}");
1844 assert!(
1845 matches!(&*prog[0].expr, Expr::If(_)),
1846 "{level:?}: dynamic condition must not eliminate branch, got {:?}",
1847 prog[0].expr
1848 );
1849 }
1850 }
1851
1852 #[test]
1853 fn elif_first_true_branch_wins() {
1854 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1856 let prog = ast_with("if (false): 1 elif (true): 2 elif (true): 3 else: 4", level);
1857 assert_eq!(prog.len(), 1, "{level:?}");
1858 assert_literal(&prog[0], "2", &format!("{level:?}"));
1859 }
1860 }
1861
1862 #[test]
1863 fn elif_all_false_no_else_collapses_to_literal_none() {
1864 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1865 let prog = ast_with("if (false): 1 elif (false): 2 elif (false): 3", level);
1866 assert_eq!(prog.len(), 1, "{level:?}");
1867 assert!(
1868 matches!(&*prog[0].expr, Expr::Literal(Literal::None)),
1869 "{level:?}: expected Literal(None), got {:?}",
1870 prog[0].expr
1871 );
1872 }
1873 }
1874
1875 #[test]
1876 fn nested_if_both_levels_eliminated() {
1877 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1879 let prog = ast_with("if (true): if (false): 1 else: 2 else: 3", level);
1880 assert_eq!(prog.len(), 1, "{level:?}");
1881 assert_literal(&prog[0], "2", &format!("{level:?}"));
1882 }
1883 }
1884
1885 #[test]
1886 fn and_falsy_literal_short_circuits_to_false() {
1887 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1889 let prog = ast_with("false && .", level);
1890 assert_eq!(prog.len(), 1, "{level:?}");
1891 assert!(
1892 matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(false))),
1893 "{level:?}: expected Literal(false), got {:?}",
1894 prog[0].expr
1895 );
1896 }
1897 }
1898
1899 #[test]
1900 fn and_truthy_literal_dropped_dynamic_preserved() {
1901 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1903 let prog = ast_with("true && .", level);
1904 assert_eq!(prog.len(), 1, "{level:?}");
1905 assert!(
1906 matches!(&*prog[0].expr, Expr::And(_)),
1907 "{level:?}: expected And([.]), got {:?}",
1908 prog[0].expr
1909 );
1910 }
1911 }
1912
1913 #[test]
1914 fn and_all_truthy_collapses_to_literal_true() {
1915 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1916 let prog = ast_with("true && true && true", level);
1917 assert_eq!(prog.len(), 1, "{level:?}");
1918 assert!(
1919 matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(true))),
1920 "{level:?}: expected Literal(true), got {:?}",
1921 prog[0].expr
1922 );
1923 }
1924 }
1925
1926 #[test]
1927 fn or_truthy_literal_short_circuits() {
1928 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1930 let prog = ast_with("true || .", level);
1931 assert_eq!(prog.len(), 1, "{level:?}");
1932 assert!(
1933 matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(true))),
1934 "{level:?}: expected Literal(true), got {:?}",
1935 prog[0].expr
1936 );
1937 }
1938 }
1939
1940 #[test]
1941 fn or_falsy_literal_dropped_dynamic_preserved() {
1942 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1944 let prog = ast_with("false || .", level);
1945 assert_eq!(prog.len(), 1, "{level:?}");
1946 assert!(
1947 matches!(&*prog[0].expr, Expr::Or(_)),
1948 "{level:?}: expected Or([.]), got {:?}",
1949 prog[0].expr
1950 );
1951 }
1952 }
1953
1954 #[test]
1955 fn or_all_falsy_collapses_to_literal_false() {
1956 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1957 let prog = ast_with("false || false || false", level);
1958 assert_eq!(prog.len(), 1, "{level:?}");
1959 assert!(
1960 matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(false))),
1961 "{level:?}: expected Literal(false), got {:?}",
1962 prog[0].expr
1963 );
1964 }
1965 }
1966
1967 #[rstest]
1968 #[case(".h1 | .text", 2usize)]
1969 #[case(".h1 | .text | .code", 3usize)]
1970 fn consecutive_selectors_merged_into_chain(#[case] query: &str, #[case] expected_len: usize) {
1971 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1972 let prog = ast_with(query, level);
1973 assert_eq!(prog.len(), 1, "{level:?}: expected single SelectorChain node");
1974 assert!(
1975 matches!(&*prog[0].expr, Expr::SelectorChain(c) if c.len() == expected_len),
1976 "{level:?}: expected SelectorChain(len={expected_len}), got {:?}",
1977 prog[0].expr
1978 );
1979 }
1980 }
1981
1982 #[test]
1983 fn single_selector_stays_as_selector_not_chain() {
1984 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1985 let prog = ast_with(".h1", level);
1986 assert_eq!(prog.len(), 1, "{level:?}");
1987 assert!(
1988 matches!(&*prog[0].expr, Expr::Selector(_)),
1989 "{level:?}: single selector must NOT become SelectorChain"
1990 );
1991 }
1992 }
1993
1994 #[rstest]
1995 #[case(".h1 | to_string | .text")]
1996 #[case(".h1 | len() | .text")]
1997 fn call_between_selectors_prevents_chain_merge(#[case] query: &str) {
1998 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
1999 let prog = ast_with(query, level);
2000 assert!(prog.len() > 1, "{level:?}: call between selectors must break the chain");
2001 assert!(
2002 !matches!(&*prog[0].expr, Expr::SelectorChain(_)),
2003 "{level:?}: must not merge selectors across a call"
2004 );
2005 }
2006 }
2007
2008 #[test]
2009 fn none_level_does_not_merge_selectors() {
2010 let prog = ast_none(".h1 | .text");
2011 assert_eq!(prog.len(), 2, "None must not merge consecutive selectors");
2012 assert!(matches!(&*prog[0].expr, Expr::Selector(_)));
2013 assert!(matches!(&*prog[1].expr, Expr::Selector(_)));
2014 }
2015
2016 #[test]
2017 fn selector_chain_inside_def_body_merged() {
2018 let prog = ast_full("def extract: .h1 | .text; | extract()");
2022 assert!(
2023 prog.iter().any(|n| matches!(&*n.expr, Expr::SelectorChain(_))),
2024 "Full: inlined extract() must produce a top-level SelectorChain"
2025 );
2026 assert!(
2027 !prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))),
2028 "Full: fully-inlined Def must be eliminated"
2029 );
2030 }
2031
2032 #[rstest]
2033 #[case("s\"hello world\"", "hello world")]
2034 #[case("s\"static text only\"", "static text only")]
2035 fn all_text_interpolated_string_folded_to_literal(#[case] query: &str, #[case] expected: &str) {
2036 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2037 let prog = ast_with(query, level);
2038 assert_eq!(prog.len(), 1, "{level:?}");
2039 assert!(
2040 matches!(&*prog[0].expr, Expr::Literal(_)),
2041 "{level:?}: all-text interpolated string must fold to Literal"
2042 );
2043 assert_literal(&prog[0], expected, &format!("{level:?}"));
2044 }
2045 }
2046
2047 #[test]
2048 fn interpolated_string_with_dynamic_expr_not_folded() {
2049 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2051 let prog = ast_with("s\"${self} end\"", level);
2052 assert_eq!(prog.len(), 1, "{level:?}");
2053 assert!(
2054 matches!(&*prog[0].expr, Expr::InterpolatedString(_)),
2055 "{level:?}: dynamic segment must prevent folding to Literal"
2056 );
2057 }
2058 }
2059
2060 #[test]
2061 fn none_level_does_not_fold_interpolated_string() {
2062 let prog = ast_none("s\"hello world\"");
2064 assert_eq!(prog.len(), 1);
2065 assert!(
2066 matches!(&*prog[0].expr, Expr::InterpolatedString(_)),
2067 "None must not fold interpolated strings"
2068 );
2069 }
2070
2071 #[test]
2072 fn let_literal_propagated_and_folded_in_full() {
2073 let prog = ast_full("let x = 5 | x + 1");
2075 assert_eq!(prog.len(), 2);
2076 assert_literal(&prog[1], "6", "Full: x+1 after propagation");
2077 }
2078
2079 #[test]
2080 fn let_literal_not_propagated_in_basic() {
2081 let prog = ast_basic("let x = 5 | x + 1");
2083 assert_eq!(prog.len(), 2);
2084 assert!(
2085 matches!(&*prog[1].expr, Expr::Call(..)),
2086 "Basic must not propagate let-literals; expected Call, got {:?}",
2087 prog[1].expr
2088 );
2089 }
2090
2091 #[test]
2092 fn let_rebind_propagates_latest_value() {
2093 let prog = ast_full("let x = 1 | let x = 2 | x + 0");
2095 assert_eq!(prog.len(), 3);
2096 assert_literal(&prog[2], "2", "Full: second binding must shadow first");
2097 }
2098
2099 #[test]
2100 fn let_multiple_bindings_all_propagated() {
2101 let prog = ast_full("let a = 2 | let b = 3 | let c = 4 | a + b + c");
2103 assert_eq!(prog.len(), 4);
2104 assert_literal(&prog[3], "9", "Full: a+b+c after propagation");
2105 }
2106
2107 #[test]
2108 fn let_non_literal_rhs_not_propagated() {
2109 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2113 let prog = ast_with("let x = add(1, .) | x + 0", level);
2114 assert_eq!(prog.len(), 2, "{level:?}");
2115 assert!(
2116 !matches!(&*prog[1].expr, Expr::Literal(_)),
2117 "{level:?}: non-literal let must not propagate to a literal, got {:?}",
2118 prog[1].expr
2119 );
2120 }
2121 }
2122
2123 #[test]
2124 fn simple_function_inlined_and_folded_in_full() {
2125 let prog = ast_full("def double(x): x * 2; | double(4)");
2127 let last = prog.last().unwrap();
2128 assert!(
2129 matches!(&*last.expr, Expr::Literal(_)),
2130 "Full: inlined+folded call must be Literal, got {:?}",
2131 last.expr
2132 );
2133 assert_literal(last, "8", "Full: double(4)");
2134 }
2135
2136 #[test]
2137 fn function_call_stays_as_call_in_basic() {
2138 let prog = ast_basic("def double(x): x * 2; | double(4)");
2140 let last = prog.last().unwrap();
2141 assert!(
2142 matches!(&*last.expr, Expr::Call(..)),
2143 "Basic must not inline; expected Call, got {:?}",
2144 last.expr
2145 );
2146 }
2147
2148 #[test]
2149 fn zero_param_constant_alias_inlined_in_full() {
2150 let prog = ast_full("def pi: 3; | pi()");
2151 let last = prog.last().unwrap();
2152 assert!(
2153 matches!(&*last.expr, Expr::Literal(_)),
2154 "Full: 0-param constant alias must inline to Literal, got {:?}",
2155 last.expr
2156 );
2157 assert_literal(last, "3", "Full: pi()");
2158 }
2159
2160 #[test]
2161 fn recursive_function_not_inlined() {
2162 let prog = ast_full("def fact(n): if (n == 0): 1 else: n * fact(n - 1); | fact(5)");
2164 let last = prog.last().unwrap();
2165 assert!(
2166 matches!(&*last.expr, Expr::Call(..)),
2167 "Full: recursive function must not be inlined; expected Call, got {:?}",
2168 last.expr
2169 );
2170 }
2171
2172 #[test]
2173 fn function_with_free_variable_not_inlined() {
2174 let prog = ast_full("let k = 10 | def add_k(x): x + k; | add_k(5)");
2176 let last = prog.last().unwrap();
2177 assert!(
2178 matches!(&*last.expr, Expr::Call(..)),
2179 "Full: function with free var must not be inlined; expected Call, got {:?}",
2180 last.expr
2181 );
2182 }
2183
2184 #[test]
2185 fn chained_inlining_collapses_to_literal() {
2186 let prog = ast_full("def add1(x): x + 1; | def mul2(x): x * 2; | mul2(add1(3))");
2188 let last = prog.last().unwrap();
2189 assert!(
2190 matches!(&*last.expr, Expr::Literal(_)),
2191 "Full: chained inline+fold must collapse to Literal, got {:?}",
2192 last.expr
2193 );
2194 assert_literal(last, "8", "Full: mul2(add1(3))");
2195 }
2196
2197 #[test]
2198 fn tco_tail_recursive_def_gets_loop_in_full() {
2199 let prog = ast_full("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);");
2200 let Expr::Def(_, _, body) = &*prog[0].expr else {
2201 panic!("expected Def");
2202 };
2203 assert!(
2204 body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))),
2205 "Full: TCO-transformed Def must contain a Loop node"
2206 );
2207 assert!(
2209 !body.iter().any(|n| matches!(&*n.expr, Expr::If(_))),
2210 "Full: original If must be replaced by Loop after TCO"
2211 );
2212 }
2213
2214 #[test]
2215 fn tco_not_applied_in_basic() {
2216 let prog = ast_basic("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);");
2217 let Expr::Def(_, _, body) = &*prog[0].expr else {
2218 panic!("expected Def");
2219 };
2220 assert!(
2221 !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))),
2222 "Basic must not apply TCO; Loop found unexpectedly"
2223 );
2224 }
2225
2226 #[test]
2227 fn tco_not_applied_in_none() {
2228 let prog = ast_none("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);");
2229 let Expr::Def(_, _, body) = &*prog[0].expr else {
2230 panic!("expected Def");
2231 };
2232 assert!(
2233 !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))),
2234 "None must not apply TCO"
2235 );
2236 }
2237
2238 #[test]
2239 fn tco_not_applied_to_non_tail_call() {
2240 let prog = ast_full("def fact(n): if (n == 0): 1 else: n * fact(n - 1);");
2242 let Expr::Def(_, _, body) = &*prog[0].expr else {
2243 panic!("expected Def");
2244 };
2245 assert!(
2246 !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))),
2247 "Full: non-tail-recursive function must not be TCO-transformed"
2248 );
2249 }
2250
2251 #[test]
2252 fn tco_multi_param_def_gets_loop() {
2253 let prog = ast_full("def loop2(a, b): if (a == 0): b else: loop2(a - 1, b + 1);");
2254 let Expr::Def(_, _, body) = &*prog[0].expr else {
2255 panic!("expected Def");
2256 };
2257 assert!(
2258 body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))),
2259 "Full: multi-param tail-recursive Def must contain Loop"
2260 );
2261 }
2262
2263 #[test]
2264 fn inline_then_constant_fold() {
2265 let prog = ast_full("def double(x): x * 2; | double(3 + 4)");
2267 let last = prog.last().unwrap();
2268 assert_literal(last, "14", "Full: double(3+4)");
2269 }
2270
2271 #[test]
2272 fn inline_reveals_dead_branch() {
2273 let prog = ast_full("def always_false(x): x == 999; | if (always_false(0)): \"bad\" else: \"good\"");
2275 let last = prog.last().unwrap();
2276 assert!(
2277 matches!(&*last.expr, Expr::Literal(_)),
2278 "Full: inline+dead-branch must collapse to Literal, got {:?}",
2279 last.expr
2280 );
2281 assert_literal(last, "good", "Full: always_false(0)");
2282 }
2283
2284 #[test]
2285 fn let_propagation_enables_inline_and_fold() {
2286 let prog = ast_full("def inc(x): x + 1; | let n = 9 | inc(n)");
2288 let last = prog.last().unwrap();
2289 assert!(
2290 matches!(&*last.expr, Expr::Literal(_)),
2291 "Full: propagation+inline+fold must collapse to Literal, got {:?}",
2292 last.expr
2293 );
2294 assert_literal(last, "10", "Full: inc(n) where n=9");
2295 }
2296
2297 #[test]
2298 fn fold_then_dead_branch_elimination() {
2299 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2301 let prog = ast_with("if (1 + 2 == 3): \"yes\" else: \"no\"", level);
2302 assert_eq!(prog.len(), 1, "{level:?}");
2303 assert_literal(&prog[0], "yes", &format!("{level:?}"));
2304 }
2305 }
2306
2307 #[test]
2308 fn let_propagation_into_and_or_operands() {
2309 let prog = ast_full("let n = 0 | n == 0 && true");
2312 let last = prog.last().unwrap();
2313 assert!(
2314 matches!(&*last.expr, Expr::Literal(Literal::Bool(true))),
2315 "Full: propagation+and fold must collapse to Literal(true), got {:?}",
2316 last.expr
2317 );
2318 }
2319
2320 #[rstest]
2321 #[case("floor(3.9)", "3")]
2322 #[case("ceil(3.1)", "4")]
2323 #[case("round(3.5)", "4")]
2324 #[case("abs(-7)", "7")]
2325 #[case("trunc(3.9)", "3")]
2326 fn numeric_unary_folds(#[case] query: &str, #[case] expected: &str) {
2327 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2328 let prog = ast_with(query, level);
2329 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2330 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2331 }
2332 }
2333
2334 #[rstest]
2335 #[case("len(\"hello\")", "5")]
2336 #[case("len(\"\")", "0")]
2337 fn len_string_folds(#[case] query: &str, #[case] expected: &str) {
2338 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2339 let prog = ast_with(query, level);
2340 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2341 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2342 }
2343 }
2344
2345 #[rstest]
2346 #[case("to_string(42)", "42")]
2347 #[case("to_string(3.5)", "3.5")]
2348 #[case("to_string(true)", "true")]
2349 #[case("to_string(false)", "false")]
2350 fn to_string_folds(#[case] query: &str, #[case] expected: &str) {
2351 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2352 let prog = ast_with(query, level);
2353 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2354 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2355 }
2356 }
2357
2358 #[rstest]
2359 #[case("to_number(\"42\")", "42")]
2360 #[case("to_number(\"3.14\")", "3.14")]
2361 fn to_number_folds(#[case] query: &str, #[case] expected: &str) {
2362 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2363 let prog = ast_with(query, level);
2364 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2365 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2366 }
2367 }
2368
2369 #[test]
2370 fn to_number_invalid_string_not_folded() {
2371 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2373 let prog = ast_with("to_number(\"abc\")", level);
2374 assert_eq!(prog.len(), 1, "{level:?}");
2375 assert!(
2376 matches!(&*prog[0].expr, Expr::Call(..)),
2377 "{level:?}: unparsable to_number must stay as Call, got {:?}",
2378 prog[0].expr
2379 );
2380 }
2381 }
2382
2383 #[rstest]
2384 #[case("lt(\"a\", \"b\")", "true")] #[case("gt(\"b\", \"a\")", "true")]
2386 #[case("lte(\"a\", \"a\")", "true")]
2387 #[case("gte(\"b\", \"a\")", "true")]
2388 #[case("lt(\"b\", \"a\")", "false")]
2389 fn string_comparison_folds(#[case] query: &str, #[case] expected: &str) {
2390 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2391 let prog = ast_with(query, level);
2392 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2393 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2394 }
2395 }
2396
2397 #[test]
2398 fn algebraic_identity_add_zero() {
2399 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2400 let prog = ast_with(". + 0", level);
2401 assert_eq!(prog.len(), 1, "{level:?}");
2402 assert!(
2403 matches!(&*prog[0].expr, Expr::Self_),
2404 "{level:?}: . + 0 must fold to Self_, got {:?}",
2405 prog[0].expr
2406 );
2407 }
2408 }
2409
2410 #[test]
2411 fn algebraic_identity_mul_zero() {
2412 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2413 let prog = ast_with(". * 0", level);
2414 assert_eq!(prog.len(), 1, "{level:?}");
2415 assert_literal(&prog[0], "0", &format!("{level:?}: . * 0"));
2416 }
2417 }
2418
2419 #[test]
2420 fn algebraic_identity_mul_one() {
2421 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2422 let prog = ast_with(". * 1", level);
2423 assert_eq!(prog.len(), 1, "{level:?}");
2424 assert!(
2425 matches!(&*prog[0].expr, Expr::Self_),
2426 "{level:?}: . * 1 must fold to Self_, got {:?}",
2427 prog[0].expr
2428 );
2429 }
2430 }
2431
2432 #[test]
2433 fn algebraic_identity_div_one() {
2434 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2435 let prog = ast_with(". / 1", level);
2436 assert_eq!(prog.len(), 1, "{level:?}");
2437 assert!(
2438 matches!(&*prog[0].expr, Expr::Self_),
2439 "{level:?}: . / 1 must fold to Self_, got {:?}",
2440 prog[0].expr
2441 );
2442 }
2443 }
2444
2445 #[rstest]
2446 #[case("trim(\" hi \")", "hi")]
2447 #[case("ltrim(\" hi\")", "hi")]
2448 #[case("rtrim(\"hi \")", "hi")]
2449 #[case("upcase(\"hello\")", "HELLO")]
2450 #[case("downcase(\"WORLD\")", "world")]
2451 fn string_transform_folds(#[case] query: &str, #[case] expected: &str) {
2452 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2453 let prog = ast_with(query, level);
2454 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2455 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2456 }
2457 }
2458
2459 #[rstest]
2460 #[case("starts_with(\"hello\", \"he\")", "true")]
2461 #[case("starts_with(\"hello\", \"wo\")", "false")]
2462 #[case("ends_with(\"hello\", \"lo\")", "true")]
2463 #[case("ends_with(\"hello\", \"he\")", "false")]
2464 #[case("index(\"hello\", \"ll\")", "2")]
2465 #[case("index(\"hello\", \"xx\")", "-1")]
2466 #[case("rindex(\"hello\", \"l\")", "3")]
2467 fn string_search_folds(#[case] query: &str, #[case] expected: &str) {
2468 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2469 let prog = ast_with(query, level);
2470 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2471 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2472 }
2473 }
2474
2475 #[rstest]
2476 #[case("replace(\"hello\", \"l\", \"r\")", "herro")]
2477 #[case("replace(\"aaa\", \"a\", \"b\")", "bbb")]
2478 fn replace_folds(#[case] query: &str, #[case] expected: &str) {
2479 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2480 let prog = ast_with(query, level);
2481 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2482 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2483 }
2484 }
2485
2486 #[test]
2487 fn coalesce_none_folded() {
2488 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2490 let prog = ast_with("coalesce(None, .)", level);
2491 assert_eq!(prog.len(), 1, "{level:?}");
2492 assert!(
2493 matches!(&*prog[0].expr, Expr::Self_),
2494 "{level:?}: coalesce(None, .) must fold to Self_, got {:?}",
2495 prog[0].expr
2496 );
2497 }
2498 }
2499
2500 #[test]
2501 fn coalesce_non_none_lhs_folded() {
2502 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2504 let prog = ast_with("coalesce(\"hi\", .)", level);
2505 assert_eq!(prog.len(), 1, "{level:?}");
2506 assert_literal(&prog[0], "hi", &format!("{level:?}: coalesce(\"hi\", .)"));
2507 }
2508 }
2509
2510 #[test]
2511 fn coalesce_both_literals_folded() {
2512 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2514 let prog = ast_with("coalesce(None, 42)", level);
2515 assert_eq!(prog.len(), 1, "{level:?}");
2516 assert_literal(&prog[0], "42", &format!("{level:?}: coalesce(None, 42)"));
2517 }
2518 }
2519
2520 #[test]
2521 fn user_def_shadows_builtin_uses_user_semantics() {
2522 let prog = ast_full("def upcase(x): x; | upcase(\"hello\")");
2526 let last = prog.last().unwrap();
2527 assert_literal(
2528 last,
2529 "hello",
2530 "Full: user-shadowed upcase must produce identity, not builtin 'HELLO'",
2531 );
2532 }
2533
2534 #[test]
2535 fn user_def_shadows_trim_uses_user_semantics() {
2536 let prog = ast_full("def trim(x): x; | trim(\" hi \")");
2539 let last = prog.last().unwrap();
2540 assert_literal(last, " hi ", "Full: user-shadowed trim must preserve spaces");
2541 }
2542
2543 #[test]
2544 fn non_shadowed_builtin_still_folded() {
2545 let prog = ast_full("def my_fn(x): x; | upcase(\"hello\")");
2547 let last = prog.last().unwrap();
2548 assert_literal(last, "HELLO", "Full: non-shadowed upcase must fold");
2549 }
2550
2551 #[rstest]
2552 #[case("trim(\" a \")", "a")]
2553 #[case("ltrim(\" a\")", "a")]
2554 #[case("rtrim(\"a \")", "a")]
2555 #[case("trim(\"\")", "")]
2556 #[case("upcase(\"abc\")", "ABC")]
2557 #[case("downcase(\"XYZ\")", "xyz")]
2558 #[case("upcase(\"123\")", "123")]
2559 fn string_ops_fold(#[case] query: &str, #[case] expected: &str) {
2560 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2561 let prog = ast_with(query, level);
2562 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2563 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2564 }
2565 }
2566
2567 #[test]
2568 fn string_ops_with_dynamic_arg_not_folded() {
2569 for q in ["trim(.)", "upcase(.)"] {
2570 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2571 let prog = ast_with(q, level);
2572 assert_eq!(prog.len(), 1, "{level:?}: {q}");
2573 assert!(
2574 matches!(&*prog[0].expr, Expr::Call(..)),
2575 "{level:?}: {q} must remain Call"
2576 );
2577 }
2578 }
2579 }
2580
2581 #[rstest]
2582 #[case("starts_with(\"hello\", \"he\")", "true")]
2583 #[case("starts_with(\"hello\", \"lo\")", "false")]
2584 #[case("ends_with(\"world\", \"ld\")", "true")]
2585 #[case("ends_with(\"world\", \"wo\")", "false")]
2586 #[case("index(\"abcabc\", \"bc\")", "1")]
2587 #[case("rindex(\"abcabc\", \"bc\")", "4")]
2588 #[case("index(\"abc\", \"x\")", "-1")]
2589 fn string_search_fold(#[case] query: &str, #[case] expected: &str) {
2590 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2591 let prog = ast_with(query, level);
2592 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2593 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2594 }
2595 }
2596
2597 #[rstest]
2598 #[case("replace(\"aabbcc\", \"b\", \"x\")", "aaxxcc")]
2599 #[case("replace(\"hello\", \"l\", \"\")", "heo")]
2600 #[case("replace(\"\", \"x\", \"y\")", "")]
2601 fn replace_fold(#[case] query: &str, #[case] expected: &str) {
2602 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2603 let prog = ast_with(query, level);
2604 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2605 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2606 }
2607 }
2608
2609 #[rstest]
2610 #[case("floor(3.7)", "3")]
2611 #[case("floor(-1.2)", "-2")]
2612 #[case("ceil(3.1)", "4")]
2613 #[case("ceil(-1.8)", "-1")]
2614 #[case("round(2.5)", "3")]
2615 #[case("round(2.4)", "2")]
2616 #[case("abs(-5)", "5")]
2617 #[case("abs(5)", "5")]
2618 #[case("trunc(3.9)", "3")]
2619 #[case("trunc(-3.9)", "-3")]
2620 fn numeric_math_folds(#[case] query: &str, #[case] expected: &str) {
2621 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2622 let prog = ast_with(query, level);
2623 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2624 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2625 }
2626 }
2627
2628 #[test]
2629 fn numeric_math_dynamic_not_folded() {
2630 for q in ["floor(.)", "abs(.)"] {
2631 let prog = ast_basic(q);
2632 assert!(
2633 matches!(&*prog[0].expr, Expr::Call(..)),
2634 "{q} with dynamic arg must stay Call"
2635 );
2636 }
2637 }
2638
2639 #[rstest]
2640 #[case("len(\"\")", "0")]
2641 #[case("len(\"hello\")", "5")]
2642 #[case("len(\"こんにちは\")", "5")] fn len_folds(#[case] query: &str, #[case] expected: &str) {
2644 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2645 let prog = ast_with(query, level);
2646 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2647 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2648 }
2649 }
2650
2651 #[rstest]
2652 #[case("to_string(0)", "0")]
2653 #[case("to_string(\"hi\")", "hi")]
2654 #[case("to_string(true)", "true")]
2655 fn to_string_lit_folds(#[case] query: &str, #[case] expected: &str) {
2656 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2657 let prog = ast_with(query, level);
2658 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2659 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2660 }
2661 }
2662
2663 #[rstest]
2664 #[case("to_number(\"-1\")", "-1")]
2665 #[case("to_number(\"0.5\")", "0.5")]
2666 fn to_number_lit_folds(#[case] query: &str, #[case] expected: &str) {
2667 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2668 let prog = ast_with(query, level);
2669 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2670 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2671 }
2672 }
2673
2674 #[rstest]
2675 #[case(". + 0")]
2676 #[case("0 + .")]
2677 #[case(". - 0")]
2678 #[case(". * 1")]
2679 #[case("1 * .")]
2680 #[case(". / 1")]
2681 fn algebraic_identity_returns_self(#[case] query: &str) {
2682 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2683 let prog = ast_with(query, level);
2684 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2685 assert!(
2686 matches!(&*prog[0].expr, Expr::Self_),
2687 "{level:?}: {query} must fold to Self_"
2688 );
2689 }
2690 }
2691
2692 #[rstest]
2693 #[case(". * 0")]
2694 #[case("0 * .")]
2695 fn algebraic_mul_zero_returns_literal_zero(#[case] query: &str) {
2696 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2697 let prog = ast_with(query, level);
2698 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2699 assert_literal(&prog[0], "0", &format!("{level:?}: {query}"));
2700 }
2701 }
2702
2703 #[rstest]
2704 #[case(". + \"\"")]
2705 #[case("\"\" + .")]
2706 fn algebraic_add_empty_string_returns_self(#[case] query: &str) {
2707 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2708 let prog = ast_with(query, level);
2709 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2710 assert!(
2711 matches!(&*prog[0].expr, Expr::Self_),
2712 "{level:?}: {query} must fold to Self_"
2713 );
2714 }
2715 }
2716
2717 #[test]
2718 fn constant_fold_inside_block() {
2719 let prog = ast_basic("(1 + 2)");
2721 assert_eq!(prog.len(), 1);
2722 assert_literal(&prog[0], "3", "Basic: (1+2) inside Paren must fold");
2723 }
2724
2725 #[test]
2726 fn selector_chain_merged_inside_while_body() {
2727 let prog = ast_basic("def f: .h1 | .text;");
2731 let Expr::Def(_, _, body) = &*prog[0].expr else {
2732 panic!("expected Def");
2733 };
2734 assert!(
2735 body.iter().any(|n| matches!(&*n.expr, Expr::SelectorChain(_))),
2736 "Basic: SelectorChain must be merged inside Def body"
2737 );
2738 }
2739
2740 #[test]
2741 fn nested_let_literal_not_propagated_across_scope() {
2742 let prog = ast_full("let x = 99 | def f: x;");
2745 let Expr::Def(_, _, body) = &*prog.iter().find(|n| matches!(&*n.expr, Expr::Def(..))).unwrap().expr else {
2746 panic!("expected Def");
2747 };
2748 assert!(
2749 matches!(&*body[0].expr, Expr::Ident(_)),
2750 "Full: top-level let must not propagate into def body, got {:?}",
2751 body[0].expr
2752 );
2753 }
2754
2755 #[test]
2756 fn inlined_def_is_eliminated() {
2757 let prog = ast_full("def double(x): x * 2; | double(5)");
2759 assert!(
2760 !prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))),
2761 "Full: fully-inlined Def must be eliminated from the program"
2762 );
2763 let last = prog.last().unwrap();
2764 assert_literal(last, "10", "Full: double(5) must fold to 10");
2765 }
2766
2767 #[test]
2768 fn non_inlinable_def_preserved() {
2769 let prog = ast_full("def count(n): if (n == 0): 0 else: count(n - 1);");
2771 assert!(
2772 prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))),
2773 "Full: non-inlinable Def must be preserved"
2774 );
2775 }
2776
2777 #[test]
2778 fn def_passed_as_value_not_eliminated() {
2779 let prog = ast_full("def is_pos(x): gt(x, 0); | filter(array(1, -1, 2), is_pos)");
2781 assert!(
2782 prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))),
2783 "Full: Def passed as first-class value must be preserved"
2784 );
2785 }
2786
2787 #[test]
2788 fn tco_only_in_full() {
2789 let query = "def sum(n): if (n == 0): 0 else: sum(n - 1);";
2790 let none_prog = ast_none(query);
2791 let basic_prog = ast_basic(query);
2792 let full_prog = ast_full(query);
2793
2794 let has_loop = |prog: &crate::ast::Program| {
2795 prog.iter().any(|n| {
2796 if let Expr::Def(_, _, body) = &*n.expr {
2797 body.iter().any(|b| matches!(&*b.expr, Expr::Loop(_)))
2798 } else {
2799 false
2800 }
2801 })
2802 };
2803 assert!(!has_loop(&none_prog), "None: no TCO");
2804 assert!(!has_loop(&basic_prog), "Basic: no TCO");
2805 assert!(has_loop(&full_prog), "Full: TCO must apply");
2806 }
2807
2808 #[test]
2809 fn none_level_is_identity() {
2810 let queries = ["1 + 2", "if (true): 1 else: 2", "false && .", "def f(x): x + 1; | f(5)"];
2812 for q in queries {
2813 let none = ast_none(q);
2814 let basic = ast_basic(q);
2815 assert!(!none.is_empty(), "None: {q} must produce nodes");
2816 assert!(none.len() >= basic.len(), "None: {q}");
2817 }
2818 }
2819
2820 #[test]
2821 fn chained_string_ops_fold() {
2822 let prog = ast_full("upcase(trim(\" hello \"))");
2824 assert_eq!(prog.len(), 1);
2825 assert_literal(&prog[0], "HELLO", "Full: chained upcase(trim(...)) must fold");
2826 }
2827
2828 #[test]
2829 fn nested_arithmetic_folds() {
2830 let prog = ast_full("floor(abs(-3.7) + 1)");
2832 assert_eq!(prog.len(), 1);
2833 assert_literal(&prog[0], "4", "Full: nested floor(abs+add) must fold");
2834 }
2835
2836 #[test]
2837 fn let_chain_propagation() {
2838 let prog = ast_full("let a = 1 | let b = 2 | a + b");
2840 let last = prog.last().unwrap();
2841 assert_literal(last, "3", "Full: chained let propagation must fold to 3");
2842 }
2843
2844 #[test]
2845 fn let_and_string_propagation() {
2846 let prog = ast_full("let prefix = \"Hello\" | starts_with(prefix, \"He\")");
2848 let last = prog.last().unwrap();
2849 assert_literal(last, "true", "Full: let+starts_with must fold");
2850 }
2851
2852 #[test]
2853 fn full_pipeline_fold_then_dead_branch() {
2854 let prog = ast_full("let x = 5 | if (x == 5): \"yes\" else: \"no\"");
2857 let last = prog.last().unwrap();
2858 assert_literal(last, "yes", "Full: propagate+fold+dead-branch must yield \"yes\"");
2859 }
2860
2861 #[test]
2862 fn basic_does_not_propagate_let() {
2863 let prog = ast_basic("let x = 5 | if (x == 5): \"yes\" else: \"no\"");
2865 let last = prog.last().unwrap();
2866 assert!(
2867 matches!(&*last.expr, Expr::If(_)),
2868 "Basic: let propagation must not happen, expected If, got {:?}",
2869 last.expr
2870 );
2871 }
2872
2873 #[test]
2874 fn module_scoped_def_not_mistaken_for_builtin_shadow() {
2875 let prog = ast_full("upcase(\"hello\") | module m: def upcase(x): x; end");
2878 let first = &prog[0];
2880 assert_literal(
2881 first,
2882 "HELLO",
2883 "Full: module-internal def must not shadow top-level fold",
2884 );
2885 }
2886
2887 #[rstest]
2889 #[case("len(b\"hello\")", "5")]
2890 #[case("len(b\"\")", "0")]
2891 fn len_bytes_folds(#[case] query: &str, #[case] expected: &str) {
2892 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2893 let prog = ast_with(query, level);
2894 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2895 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2896 }
2897 }
2898
2899 #[rstest]
2901 #[case("to_string(None)", "")]
2902 fn to_string_none_folds(#[case] query: &str, #[case] expected: &str) {
2903 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2904 let prog = ast_with(query, level);
2905 assert_eq!(prog.len(), 1, "{level:?}: {query}");
2906 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
2907 }
2908 }
2909
2910 #[test]
2912 fn to_string_bytes_not_folded() {
2913 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2914 let prog = ast_with("to_string(b\"hi\")", level);
2915 assert_eq!(prog.len(), 1, "{level:?}");
2916 assert!(
2917 matches!(&*prog[0].expr, Expr::Call(..)),
2918 "{level:?}: to_string(bytes) must stay as Call"
2919 );
2920 }
2921 }
2922
2923 #[test]
2925 fn nan_arithmetic_not_folded() {
2926 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2928 let prog = ast_with("floor(nan())", level);
2929 assert_eq!(prog.len(), 1, "{level:?}");
2930 assert!(
2931 matches!(&*prog[0].expr, Expr::Call(..)),
2932 "{level:?}: floor(nan()) must not fold"
2933 );
2934 }
2935 }
2936
2937 #[test]
2939 fn let_propagation_into_try_block() {
2940 let prog = ast_full("let x = 5 | try: x catch: 0");
2942 assert_eq!(prog.len(), 2, "Full: let + try must produce 2 nodes");
2943 }
2944
2945 #[test]
2946 fn let_propagation_into_interpolated_string() {
2947 let prog = ast_full("let x = \"hi\" | s\"${x} world\"");
2949 let last = prog.last().unwrap();
2950 assert_literal(last, "hi world", "Full: let+interpolated string must fold");
2951 }
2952
2953 #[test]
2954 fn let_propagation_into_paren() {
2955 let prog = ast_full("let x = 3 | (x + 1)");
2957 let last = prog.last().unwrap();
2958 assert_literal(last, "4", "Full: let+paren must fold");
2959 }
2960
2961 #[test]
2963 fn two_call_sites_both_inlined_def_eliminated() {
2964 let prog = ast_full("def inc(x): x + 1; | inc(3) | inc(7)");
2966 assert!(
2967 !prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))),
2968 "Full: Def with two inlined call sites must be eliminated"
2969 );
2970 let last = prog.last().unwrap();
2971 assert_literal(last, "8", "Full: inc(7) must fold to 8");
2972 }
2973
2974 #[test]
2976 fn optimize_while_folds_constant_in_body() {
2977 let prog = ast_basic("while(true): 1 + 1;");
2979 assert_eq!(prog.len(), 1);
2980 assert!(
2982 matches!(&*prog[0].expr, Expr::While(..)),
2983 "Basic: while must remain when condition is dynamic-ish"
2984 );
2985 }
2986
2987 #[test]
2988 fn optimize_try_with_constant_folds_body() {
2989 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
2991 let prog = ast_with("try: 1 + 2 catch: 0", level);
2992 assert_eq!(prog.len(), 1, "{level:?}");
2993 assert!(
2995 matches!(&*prog[0].expr, Expr::Try(..)),
2996 "{level:?}: Try must remain; got {:?}",
2997 prog[0].expr
2998 );
2999 }
3000 }
3001
3002 #[test]
3003 fn optimize_foreach_folds_constant_values() {
3004 let prog = ast_basic("foreach(x, [1]): 2 + 3;");
3006 assert_eq!(prog.len(), 1, "Basic: foreach must be single node");
3007 let Expr::Foreach(_, _, body) = &*prog[0].expr else {
3008 panic!("expected Foreach");
3009 };
3010 assert!(
3011 body.iter().any(|n| matches!(&*n.expr, Expr::Literal(_))),
3012 "Basic: Foreach body must have folded constant"
3013 );
3014 }
3015
3016 #[test]
3017 fn optimize_match_folds_constant_in_arms() {
3018 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
3020 let prog = ast_with("match(1 + 2) do | 3: \"three\" | _: \"other\" end", level);
3021 assert_eq!(prog.len(), 1, "{level:?}");
3022 assert!(
3024 matches!(&*prog[0].expr, Expr::Match(..)),
3025 "{level:?}: Match must remain when value is not a pattern-eliminating literal"
3026 );
3027 }
3028 }
3029
3030 #[test]
3032 fn algebraic_identity_sub_zero() {
3033 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
3034 let prog = ast_with(". - 0", level);
3035 assert_eq!(prog.len(), 1, "{level:?}");
3036 assert!(
3037 matches!(&*prog[0].expr, Expr::Self_),
3038 "{level:?}: . - 0 must fold to Self_"
3039 );
3040 }
3041 }
3042
3043 #[rstest]
3045 #[case("eq(1, \"one\")", "false")]
3046 #[case("ne(1, \"one\")", "true")]
3047 #[case("eq(None, None)", "true")]
3048 #[case("ne(None, 0)", "true")]
3049 fn eq_ne_cross_type_fold(#[case] query: &str, #[case] expected: &str) {
3050 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
3051 let prog = ast_with(query, level);
3052 assert_eq!(prog.len(), 1, "{level:?}: {query}");
3053 assert_literal(&prog[0], expected, &format!("{level:?}: {query}"));
3054 }
3055 }
3056
3057 #[test]
3059 fn mixed_inline_and_non_inline_defs() {
3060 let prog = ast_full(
3062 "def double(x): x * 2; | def fact(n): if (n == 0): 1 else: n * fact(n - 1); | double(3) | fact(4)",
3063 );
3064 assert!(
3067 prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))),
3068 "Full: recursive fact must be preserved"
3069 );
3070 let last = prog.last().unwrap();
3071 assert!(matches!(&*last.expr, Expr::Call(..)), "Full: fact(4) must stay as Call");
3072 }
3073
3074 #[test]
3076 fn let_propagation_into_selectorchain_body() {
3077 let prog = ast_full("let x = 5 | .h1 | x + 0");
3079 let last = prog.last().unwrap();
3081 assert_literal(last, "5", "Full: let + selector + x+0 must fold");
3082 }
3083
3084 #[rstest]
3086 #[case("\"\" && .", "false")] #[case("\"x\" && .", r#"."#)] fn literal_is_truthy_string(#[case] query: &str, #[case] _expected: &str) {
3089 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
3090 let prog = ast_with(query, level);
3091 assert_eq!(prog.len(), 1, "{level:?}: {query}");
3092 }
3093 }
3094
3095 #[test]
3096 fn literal_is_truthy_zero_number_is_falsy() {
3097 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
3099 let prog = ast_with("0 && .", level);
3100 assert_eq!(prog.len(), 1, "{level:?}");
3101 assert!(
3102 matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(false))),
3103 "{level:?}: 0 && . must short-circuit to false"
3104 );
3105 }
3106 }
3107
3108 #[test]
3109 fn literal_is_truthy_nonzero_number_is_truthy() {
3110 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
3112 let prog = ast_with("1 && .", level);
3113 assert_eq!(prog.len(), 1, "{level:?}");
3114 assert!(
3115 matches!(&*prog[0].expr, Expr::And(_)),
3116 "{level:?}: 1 && . truthy lit must be dropped leaving And([.])"
3117 );
3118 }
3119 }
3120
3121 #[test]
3123 fn coalesce_both_dynamic_stays_as_call() {
3124 for level in [OptimizationLevel::Basic, OptimizationLevel::Full] {
3125 let prog = ast_with("coalesce(., .)", level);
3126 assert_eq!(prog.len(), 1, "{level:?}");
3127 assert!(
3128 matches!(&*prog[0].expr, Expr::Call(..)),
3129 "{level:?}: coalesce(., .) with both dynamic must stay as Call"
3130 );
3131 }
3132 }
3133
3134 #[test]
3136 fn def_with_default_param_not_inlined() {
3137 let prog = ast_full("def greet(name, greeting = \"hello\"): greeting; | greet(\"world\")");
3138 let last = prog.last().unwrap();
3140 assert!(
3141 matches!(&*last.expr, Expr::Call(..)),
3142 "Full: def with default param must not be inlined; expected Call"
3143 );
3144 }
3145}