spo_rhai/
optimizer.rs

1//! Module implementing the [`AST`] optimizer.
2#![cfg(not(feature = "no_optimize"))]
3
4use crate::ast::{
5    ASTFlags, Expr, FlowControl, OpAssignment, Stmt, StmtBlock, StmtBlockContainer,
6    SwitchCasesCollection,
7};
8use crate::engine::{
9    KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CURRY, KEYWORD_PRINT,
10    KEYWORD_TYPE_OF, OP_NOT,
11};
12use crate::eval::{Caches, GlobalRuntimeState};
13use crate::func::builtin::get_builtin_binary_op_fn;
14use crate::func::hashing::get_hasher;
15use crate::module::ModuleFlags;
16use crate::tokenizer::Token;
17use crate::{
18    calc_fn_hash, calc_fn_hash_full, Dynamic, Engine, FnArgsVec, FnPtr, ImmutableString, Position,
19    Scope, AST,
20};
21#[cfg(feature = "no_std")]
22use std::prelude::v1::*;
23use std::{
24    any::TypeId,
25    borrow::Cow,
26    convert::TryFrom,
27    hash::{Hash, Hasher},
28    mem,
29};
30
31/// Level of optimization performed.
32///
33/// Not available under `no_optimize`.
34#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, Hash)]
35#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
36#[non_exhaustive]
37pub enum OptimizationLevel {
38    /// No optimization performed.
39    None,
40    /// Only perform simple optimizations without evaluating functions.
41    #[default]
42    Simple,
43    /// Full optimizations performed, including evaluating functions.
44    /// Take care that this may cause side effects as it essentially assumes that all functions are pure.
45    Full,
46}
47
48/// Mutable state throughout an optimization pass.
49#[derive(Debug, Clone)]
50struct OptimizerState<'a> {
51    /// Has the [`AST`] been changed during this pass?
52    is_dirty: bool,
53    /// Stack of variables/constants for constants propagation and strict variables checking.
54    variables: Vec<(ImmutableString, Option<Cow<'a, Dynamic>>)>,
55    /// Activate constants propagation?
56    propagate_constants: bool,
57    /// [`Engine`] instance for eager function evaluation.
58    engine: &'a Engine,
59    /// Optional [`Scope`].
60    scope: Option<&'a Scope<'a>>,
61    /// The global runtime state.
62    global: GlobalRuntimeState,
63    /// Function resolution caches.
64    caches: Caches,
65    /// Optimization level.
66    optimization_level: OptimizationLevel,
67}
68
69impl<'a> OptimizerState<'a> {
70    /// Create a new [`OptimizerState`].
71    #[inline(always)]
72    pub fn new(
73        engine: &'a Engine,
74        lib: &'a [crate::SharedModule],
75        scope: Option<&'a Scope<'a>>,
76        optimization_level: OptimizationLevel,
77    ) -> Self {
78        let mut _global = GlobalRuntimeState::new(engine);
79        let _lib = lib;
80
81        #[cfg(not(feature = "no_function"))]
82        {
83            _global.lib = _lib.into();
84        }
85
86        Self {
87            is_dirty: false,
88            variables: Vec::new(),
89            propagate_constants: true,
90            engine,
91            scope,
92            global: _global,
93            caches: Caches::new(),
94            optimization_level,
95        }
96    }
97    /// Set the [`AST`] state to be dirty (i.e. changed).
98    #[inline(always)]
99    pub fn set_dirty(&mut self) {
100        self.is_dirty = true;
101    }
102    /// Set the [`AST`] state to be not dirty (i.e. unchanged).
103    #[inline(always)]
104    pub fn clear_dirty(&mut self) {
105        self.is_dirty = false;
106    }
107    /// Is the [`AST`] dirty (i.e. changed)?
108    #[inline(always)]
109    pub const fn is_dirty(&self) -> bool {
110        self.is_dirty
111    }
112    /// Rewind the variables stack back to a specified size.
113    #[inline(always)]
114    pub fn rewind_var(&mut self, len: usize) {
115        self.variables.truncate(len);
116    }
117    /// Add a new variable to the stack.
118    ///
119    /// `Some(value)` if literal constant (which can be used for constants propagation), `None` otherwise.
120    #[inline(always)]
121    pub fn push_var<'x: 'a>(&mut self, name: ImmutableString, value: Option<Cow<'x, Dynamic>>) {
122        self.variables.push((name, value));
123    }
124    /// Look up a literal constant from the variables stack.
125    #[inline]
126    pub fn find_literal_constant(&self, name: &str) -> Option<&Dynamic> {
127        self.variables
128            .iter()
129            .rev()
130            .find(|(n, _)| n == name)
131            .and_then(|(_, value)| value.as_deref())
132    }
133    /// Call a registered function
134    #[inline]
135    pub fn call_fn_with_const_args(
136        &mut self,
137        fn_name: &str,
138        op_token: Option<&Token>,
139        arg_values: &mut [Dynamic],
140    ) -> Option<Dynamic> {
141        self.engine
142            .exec_native_fn_call(
143                &mut self.global,
144                &mut self.caches,
145                fn_name,
146                op_token,
147                calc_fn_hash(None, fn_name, arg_values.len()),
148                &mut arg_values.iter_mut().collect::<FnArgsVec<_>>(),
149                false,
150                true,
151                Position::NONE,
152            )
153            .ok()
154            .map(|(v, ..)| v)
155    }
156}
157
158/// Optimize a block of [statements][Stmt].
159fn optimize_stmt_block(
160    mut statements: StmtBlockContainer,
161    state: &mut OptimizerState,
162    preserve_result: bool,
163    is_internal: bool,
164    reduce_return: bool,
165) -> StmtBlockContainer {
166    if statements.is_empty() {
167        return statements;
168    }
169
170    let mut is_dirty = state.is_dirty();
171
172    let is_pure = if is_internal {
173        Stmt::is_internally_pure
174    } else {
175        Stmt::is_pure
176    };
177
178    // Flatten blocks
179    while let Some(n) = statements.iter().position(
180        |s| matches!(s, Stmt::Block(block, ..) if !block.iter().any(Stmt::is_block_dependent)),
181    ) {
182        let (first, second) = statements.split_at_mut(n);
183        let mut stmt = second[0].take();
184        let stmts = match stmt {
185            Stmt::Block(ref mut block, ..) => block.statements_mut(),
186            _ => unreachable!("Stmt::Block expected but gets {:?}", stmt),
187        };
188        statements = first
189            .iter_mut()
190            .map(mem::take)
191            .chain(stmts.iter_mut().map(mem::take))
192            .chain(second.iter_mut().skip(1).map(mem::take))
193            .collect();
194
195        is_dirty = true;
196    }
197
198    // Optimize
199    loop {
200        state.clear_dirty();
201
202        let orig_constants_len = state.variables.len(); // Original number of constants in the state, for restore later
203        let orig_propagate_constants = state.propagate_constants;
204
205        // Remove everything following control flow breaking statements
206        let mut dead_code = false;
207
208        statements.retain(|stmt| {
209            if dead_code {
210                state.set_dirty();
211                false
212            } else if stmt.is_control_flow_break() {
213                dead_code = true;
214                true
215            } else {
216                true
217            }
218        });
219
220        // Optimize each statement in the block
221        statements.iter_mut().for_each(|stmt| {
222            match stmt {
223                Stmt::Var(x, options, ..) => {
224                    optimize_expr(&mut x.1, state, false);
225
226                    let value = if options.intersects(ASTFlags::CONSTANT) && x.1.is_constant() {
227                        // constant literal
228                        Some(Cow::Owned(x.1.get_literal_value().unwrap()))
229                    } else {
230                        // variable
231                        None
232                    };
233                    state.push_var(x.0.name.clone(), value);
234                }
235                // Optimize the statement
236                _ => optimize_stmt(stmt, state, preserve_result),
237            }
238        });
239
240        // Remove all pure statements except the last one
241        let mut index = 0;
242        let mut first_non_constant = statements
243            .iter()
244            .rev()
245            .position(|stmt| match stmt {
246                stmt if !is_pure(stmt) => true,
247
248                Stmt::Var(x, ..) if x.1.is_constant() => true,
249                Stmt::Expr(e) if !e.is_constant() => true,
250
251                #[cfg(not(feature = "no_module"))]
252                Stmt::Import(x, ..) if !x.0.is_constant() => true,
253
254                _ => false,
255            })
256            .map_or(0, |n| statements.len() - n - 1);
257
258        while index < statements.len() {
259            if preserve_result && index >= statements.len() - 1 {
260                break;
261            }
262            match statements[index] {
263                ref stmt if is_pure(stmt) && index >= first_non_constant => {
264                    state.set_dirty();
265                    statements.remove(index);
266                }
267                ref stmt if stmt.is_pure() => {
268                    state.set_dirty();
269                    if index < first_non_constant {
270                        first_non_constant -= 1;
271                    }
272                    statements.remove(index);
273                }
274                _ => index += 1,
275            }
276        }
277
278        // Remove all pure statements that do not return values at the end of a block.
279        // We cannot remove anything for non-pure statements due to potential side-effects.
280        if preserve_result {
281            loop {
282                match statements[..] {
283                    // { return; } -> {}
284                    [Stmt::Return(None, options, ..)]
285                        if reduce_return && !options.intersects(ASTFlags::BREAK) =>
286                    {
287                        state.set_dirty();
288                        statements.clear();
289                    }
290                    [ref stmt] if !stmt.returns_value() && is_pure(stmt) => {
291                        state.set_dirty();
292                        statements.clear();
293                    }
294                    // { ...; return; } -> { ... }
295                    [.., ref last_stmt, Stmt::Return(None, options, ..)]
296                        if reduce_return
297                            && !options.intersects(ASTFlags::BREAK)
298                            && !last_stmt.returns_value() =>
299                    {
300                        state.set_dirty();
301                        statements.pop().unwrap();
302                    }
303                    // { ...; return val; } -> { ...; val }
304                    [.., Stmt::Return(ref mut expr, options, pos)]
305                        if reduce_return && !options.intersects(ASTFlags::BREAK) =>
306                    {
307                        state.set_dirty();
308                        *statements.last_mut().unwrap() = expr
309                            .as_mut()
310                            .map_or_else(|| Stmt::Noop(pos), |e| Stmt::Expr(mem::take(e)));
311                    }
312                    // { ...; stmt; noop } -> done
313                    [.., ref second_last_stmt, Stmt::Noop(..)]
314                        if second_last_stmt.returns_value() =>
315                    {
316                        break
317                    }
318                    // { ...; stmt_that_returns; pure_non_value_stmt } -> { ...; stmt_that_returns; noop }
319                    // { ...; stmt; pure_non_value_stmt } -> { ...; stmt }
320                    [.., ref second_last_stmt, ref last_stmt]
321                        if !last_stmt.returns_value() && is_pure(last_stmt) =>
322                    {
323                        state.set_dirty();
324                        if second_last_stmt.returns_value() {
325                            *statements.last_mut().unwrap() = Stmt::Noop(last_stmt.position());
326                        } else {
327                            statements.pop().unwrap();
328                        }
329                    }
330                    _ => break,
331                }
332            }
333        } else {
334            loop {
335                match statements[..] {
336                    [ref stmt] if is_pure(stmt) => {
337                        state.set_dirty();
338                        statements.clear();
339                    }
340                    // { ...; return; } -> { ... }
341                    [.., Stmt::Return(None, options, ..)]
342                        if reduce_return && !options.intersects(ASTFlags::BREAK) =>
343                    {
344                        state.set_dirty();
345                        statements.pop().unwrap();
346                    }
347                    // { ...; return pure_val; } -> { ... }
348                    [.., Stmt::Return(Some(ref expr), options, ..)]
349                        if reduce_return
350                            && !options.intersects(ASTFlags::BREAK)
351                            && expr.is_pure() =>
352                    {
353                        state.set_dirty();
354                        statements.pop().unwrap();
355                    }
356                    [.., ref last_stmt] if is_pure(last_stmt) => {
357                        state.set_dirty();
358                        statements.pop().unwrap();
359                    }
360                    _ => break,
361                }
362            }
363        }
364
365        // Pop the stack and remove all the local constants
366        state.rewind_var(orig_constants_len);
367        state.propagate_constants = orig_propagate_constants;
368
369        if !state.is_dirty() {
370            break;
371        }
372
373        is_dirty = true;
374    }
375
376    if is_dirty {
377        state.set_dirty();
378    }
379
380    statements.shrink_to_fit();
381    statements
382}
383
384impl StmtBlock {
385    #[inline(always)]
386    #[must_use]
387    fn take_statements(&mut self) -> StmtBlockContainer {
388        mem::take(self.statements_mut())
389    }
390}
391
392/// Is this [`Expr`] a constant that is hashable?
393#[inline(always)]
394fn is_hashable_constant(expr: &Expr) -> bool {
395    match expr {
396        _ if !expr.is_constant() => false,
397        Expr::DynamicConstant(v, ..) => v.is_hashable(),
398        _ => false,
399    }
400}
401
402/// Optimize a [statement][Stmt].
403fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: bool) {
404    #[inline(always)]
405    #[must_use]
406    fn is_variable_access(expr: &Expr, _non_qualified: bool) -> bool {
407        match expr {
408            #[cfg(not(feature = "no_module"))]
409            Expr::Variable(x, ..) if _non_qualified && !x.1.is_empty() => false,
410            Expr::Variable(..) => true,
411            _ => false,
412        }
413    }
414
415    match stmt {
416        // var = var op expr => var op= expr
417        Stmt::Assignment(x, ..)
418            if !x.0.is_op_assignment()
419                && is_variable_access(&x.1.lhs, true)
420                && matches!(&x.1.rhs, Expr::FnCall(x2, ..)
421                        if Token::lookup_symbol_from_syntax(&x2.name).map_or(false, |t| t.has_op_assignment())
422                        && x2.args.len() == 2
423                        && x2.args[0].get_variable_name(true) == x.1.lhs.get_variable_name(true)
424                ) =>
425        {
426            match x.1.rhs {
427                Expr::FnCall(ref mut x2, pos) => {
428                    state.set_dirty();
429                    x.0 = OpAssignment::new_op_assignment_from_base(&x2.name, pos);
430                    x.1.rhs = x2.args[1].take();
431                }
432                ref expr => unreachable!("Expr::FnCall expected but gets {:?}", expr),
433            }
434        }
435
436        // expr op= expr
437        Stmt::Assignment(x, ..) => {
438            if !is_variable_access(&x.1.lhs, false) {
439                optimize_expr(&mut x.1.lhs, state, false);
440            }
441            optimize_expr(&mut x.1.rhs, state, false);
442        }
443
444        // if expr {}
445        Stmt::If(x, ..) if x.body.is_empty() && x.branch.is_empty() => {
446            let condition = &mut x.expr;
447            state.set_dirty();
448
449            let pos = condition.start_position();
450            let mut expr = condition.take();
451            optimize_expr(&mut expr, state, false);
452
453            *stmt = if preserve_result {
454                // -> { expr, Noop }
455                Stmt::Block(
456                    StmtBlock::new(
457                        [Stmt::Expr(expr.into()), Stmt::Noop(pos)],
458                        pos,
459                        Position::NONE,
460                    )
461                    .into(),
462                )
463            } else {
464                // -> expr
465                Stmt::Expr(expr.into())
466            };
467        }
468        // if false { if_block } -> Noop
469        Stmt::If(x, ..)
470            if matches!(x.expr, Expr::BoolConstant(false, ..)) && x.branch.is_empty() =>
471        {
472            match x.expr {
473                Expr::BoolConstant(false, pos) => {
474                    state.set_dirty();
475                    *stmt = Stmt::Noop(pos);
476                }
477                _ => unreachable!("`Expr::BoolConstant`"),
478            }
479        }
480        // if false { if_block } else { else_block } -> else_block
481        Stmt::If(x, ..) if matches!(x.expr, Expr::BoolConstant(false, ..)) => {
482            state.set_dirty();
483            let body = x.branch.take_statements();
484            *stmt = match optimize_stmt_block(body, state, preserve_result, true, false) {
485                statements if statements.is_empty() => Stmt::Noop(x.branch.position()),
486                statements => {
487                    Stmt::Block(StmtBlock::new_with_span(statements, x.branch.span()).into())
488                }
489            }
490        }
491        // if true { if_block } else { else_block } -> if_block
492        Stmt::If(x, ..) if matches!(x.expr, Expr::BoolConstant(true, ..)) => {
493            state.set_dirty();
494            let body = x.body.take_statements();
495            *stmt = match optimize_stmt_block(body, state, preserve_result, true, false) {
496                statements if statements.is_empty() => Stmt::Noop(x.body.position()),
497                statements => {
498                    Stmt::Block(StmtBlock::new_with_span(statements, x.body.span()).into())
499                }
500            }
501        }
502        // if expr { if_block } else { else_block }
503        Stmt::If(x, ..) => {
504            let FlowControl { expr, body, branch } = &mut **x;
505            optimize_expr(expr, state, false);
506            *body.statements_mut() =
507                optimize_stmt_block(body.take_statements(), state, preserve_result, true, false);
508            *branch.statements_mut() = optimize_stmt_block(
509                branch.take_statements(),
510                state,
511                preserve_result,
512                true,
513                false,
514            );
515        }
516
517        // switch const { ... }
518        Stmt::Switch(x, pos) if is_hashable_constant(&x.0) => {
519            let (
520                match_expr,
521                SwitchCasesCollection {
522                    expressions,
523                    cases,
524                    ranges,
525                    def_case,
526                },
527            ) = &mut **x;
528
529            let value = match_expr.get_literal_value().unwrap();
530            let hasher = &mut get_hasher();
531            value.hash(hasher);
532            let hash = hasher.finish();
533
534            // First check hashes
535            if let Some(case_blocks_list) = cases.get(&hash) {
536                match &case_blocks_list[..] {
537                    [] => (),
538                    [index] => {
539                        let mut b = mem::take(&mut expressions[*index]);
540                        cases.clear();
541
542                        if matches!(b.lhs, Expr::BoolConstant(true, ..)) {
543                            // Promote the matched case
544                            let mut statements = Stmt::Expr(b.rhs.take().into());
545                            optimize_stmt(&mut statements, state, true);
546                            *stmt = statements;
547                        } else {
548                            // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def }
549                            optimize_expr(&mut b.lhs, state, false);
550
551                            let branch = def_case.map_or(StmtBlock::NONE, |index| {
552                                let mut def_stmt = Stmt::Expr(expressions[index].rhs.take().into());
553                                optimize_stmt(&mut def_stmt, state, true);
554                                def_stmt.into()
555                            });
556                            let body = Stmt::Expr(b.rhs.take().into()).into();
557                            let expr = b.lhs.take();
558
559                            *stmt = Stmt::If(
560                                FlowControl { expr, body, branch }.into(),
561                                match_expr.start_position(),
562                            );
563                        }
564
565                        state.set_dirty();
566                        return;
567                    }
568                    _ => {
569                        for &index in case_blocks_list {
570                            let mut b = mem::take(&mut expressions[index]);
571
572                            if matches!(b.lhs, Expr::BoolConstant(true, ..)) {
573                                // Promote the matched case
574                                let mut statements = Stmt::Expr(b.rhs.take().into());
575                                optimize_stmt(&mut statements, state, true);
576                                *stmt = statements;
577                                state.set_dirty();
578                                return;
579                            }
580                        }
581                    }
582                }
583            }
584
585            // Then check ranges
586            if !ranges.is_empty() {
587                // Only one range or all ranges without conditions
588                if ranges.len() == 1
589                    || ranges
590                        .iter()
591                        .all(|r| matches!(expressions[r.index()].lhs, Expr::BoolConstant(true, ..)))
592                {
593                    if let Some(r) = ranges.iter().find(|r| r.contains(&value)) {
594                        let range_block = &mut expressions[r.index()];
595
596                        if matches!(range_block.lhs, Expr::BoolConstant(true, ..)) {
597                            // Promote the matched case
598                            let block = &mut expressions[r.index()];
599                            let mut statements = Stmt::Expr(block.rhs.take().into());
600                            optimize_stmt(&mut statements, state, true);
601                            *stmt = statements;
602                        } else {
603                            let mut expr = range_block.lhs.take();
604
605                            // switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def }
606                            optimize_expr(&mut expr, state, false);
607
608                            let branch = def_case.map_or(StmtBlock::NONE, |index| {
609                                let mut def_stmt = Stmt::Expr(expressions[index].rhs.take().into());
610                                optimize_stmt(&mut def_stmt, state, true);
611                                def_stmt.into()
612                            });
613
614                            let body = Stmt::Expr(expressions[r.index()].rhs.take().into()).into();
615
616                            *stmt = Stmt::If(
617                                FlowControl { expr, body, branch }.into(),
618                                match_expr.start_position(),
619                            );
620                        }
621
622                        state.set_dirty();
623                        return;
624                    }
625                } else {
626                    // Multiple ranges - clear the table and just keep the right ranges
627                    if !cases.is_empty() {
628                        state.set_dirty();
629                        cases.clear();
630                    }
631
632                    let old_ranges_len = ranges.len();
633
634                    ranges.retain(|r| r.contains(&value));
635
636                    if ranges.len() != old_ranges_len {
637                        state.set_dirty();
638                    }
639
640                    for r in ranges {
641                        let b = &mut expressions[r.index()];
642                        optimize_expr(&mut b.lhs, state, false);
643                        optimize_expr(&mut b.rhs, state, false);
644                    }
645                    return;
646                }
647            }
648
649            // Promote the default case
650            state.set_dirty();
651
652            match def_case {
653                Some(index) => {
654                    let mut def_stmt = Stmt::Expr(expressions[*index].rhs.take().into());
655                    optimize_stmt(&mut def_stmt, state, true);
656                    *stmt = def_stmt;
657                }
658                _ => *stmt = Stmt::Block(StmtBlock::empty(*pos).into()),
659            }
660        }
661        // switch
662        Stmt::Switch(x, ..) => {
663            let (
664                match_expr,
665                SwitchCasesCollection {
666                    expressions,
667                    cases,
668                    ranges,
669                    def_case,
670                    ..
671                },
672            ) = &mut **x;
673
674            optimize_expr(match_expr, state, false);
675
676            // Optimize blocks
677            for b in &mut *expressions {
678                optimize_expr(&mut b.lhs, state, false);
679                optimize_expr(&mut b.rhs, state, false);
680
681                if matches!(b.lhs, Expr::BoolConstant(false, ..)) && !b.rhs.is_unit() {
682                    b.rhs = Expr::Unit(b.rhs.position());
683                    state.set_dirty();
684                }
685            }
686
687            // Remove false cases
688            cases.retain(|_, list| {
689                // Remove all entries that have false conditions
690                list.retain(|index| {
691                    if matches!(expressions[*index].lhs, Expr::BoolConstant(false, ..)) {
692                        state.set_dirty();
693                        false
694                    } else {
695                        true
696                    }
697                });
698                // Remove all entries after a `true` condition
699                if let Some(n) = list.iter().position(|&index| {
700                    matches!(expressions[index].lhs, Expr::BoolConstant(true, ..))
701                }) {
702                    if n + 1 < list.len() {
703                        state.set_dirty();
704                        list.truncate(n + 1);
705                    }
706                }
707                // Remove if no entry left
708                if list.is_empty() {
709                    state.set_dirty();
710                    false
711                } else {
712                    true
713                }
714            });
715
716            // Remove false ranges
717            ranges.retain(|r| {
718                if matches!(expressions[r.index()].lhs, Expr::BoolConstant(false, ..)) {
719                    state.set_dirty();
720                    false
721                } else {
722                    true
723                }
724            });
725
726            if let Some(index) = def_case {
727                optimize_expr(&mut expressions[*index].rhs, state, false);
728            }
729
730            // Remove unused block statements
731            expressions.iter_mut().enumerate().for_each(|(index, b)| {
732                if *def_case != Some(index)
733                    && cases.values().flat_map(|c| c.iter()).all(|&n| n != index)
734                    && ranges.iter().all(|r| r.index() != index)
735                    && !b.rhs.is_unit()
736                {
737                    b.rhs = Expr::Unit(b.rhs.position());
738                    state.set_dirty();
739                }
740            });
741        }
742
743        // while false { block } -> Noop
744        Stmt::While(x, ..) if matches!(x.expr, Expr::BoolConstant(false, ..)) => match x.expr {
745            Expr::BoolConstant(false, pos) => {
746                state.set_dirty();
747                *stmt = Stmt::Noop(pos);
748            }
749            _ => unreachable!("`Expr::BoolConstant"),
750        },
751        // while expr { block }
752        Stmt::While(x, ..) => {
753            let FlowControl { expr, body, .. } = &mut **x;
754            optimize_expr(expr, state, false);
755            if let Expr::BoolConstant(true, pos) = expr {
756                *expr = Expr::Unit(*pos);
757            }
758            *body.statements_mut() =
759                optimize_stmt_block(body.take_statements(), state, false, true, false);
760        }
761        // do { block } while|until expr
762        Stmt::Do(x, ..) => {
763            optimize_expr(&mut x.expr, state, false);
764            *x.body.statements_mut() =
765                optimize_stmt_block(x.body.take_statements(), state, false, true, false);
766        }
767        // for id in expr { block }
768        Stmt::For(x, ..) => {
769            optimize_expr(&mut x.2.expr, state, false);
770            *x.2.body.statements_mut() =
771                optimize_stmt_block(x.2.body.take_statements(), state, false, true, false);
772        }
773        // let id = expr;
774        Stmt::Var(x, options, ..) if !options.intersects(ASTFlags::CONSTANT) => {
775            optimize_expr(&mut x.1, state, false);
776        }
777        // import expr as var;
778        #[cfg(not(feature = "no_module"))]
779        Stmt::Import(x, ..) => optimize_expr(&mut x.0, state, false),
780        // { block }
781        Stmt::Block(block) => {
782            let mut stmts =
783                optimize_stmt_block(block.take_statements(), state, preserve_result, true, false);
784
785            match stmts.as_mut_slice() {
786                [] => {
787                    state.set_dirty();
788                    *stmt = Stmt::Noop(block.span().start());
789                }
790                // Only one statement which is not block-dependent - promote
791                [s] if !s.is_block_dependent() => {
792                    state.set_dirty();
793                    *stmt = s.take();
794                }
795                _ => *block.statements_mut() = stmts,
796            }
797        }
798        // try { pure try_block } catch ( var ) { catch_block } -> try_block
799        Stmt::TryCatch(x, ..) if x.body.iter().all(Stmt::is_pure) => {
800            // If try block is pure, there will never be any exceptions
801            state.set_dirty();
802            *x.body.statements_mut() =
803                optimize_stmt_block(x.body.take_statements(), state, false, true, false);
804        }
805        // try { try_block } catch ( var ) { catch_block }
806        Stmt::TryCatch(x, ..) => {
807            *x.body.statements_mut() =
808                optimize_stmt_block(x.body.take_statements(), state, false, true, false);
809            *x.branch.statements_mut() =
810                optimize_stmt_block(x.branch.take_statements(), state, false, true, false);
811        }
812
813        // expr(stmt)
814        Stmt::Expr(expr) if matches!(**expr, Expr::Stmt(..)) => {
815            state.set_dirty();
816            match expr.as_mut() {
817                Expr::Stmt(block) if !block.is_empty() => {
818                    let mut stmts_blk = mem::take(block.as_mut());
819                    *stmts_blk.statements_mut() =
820                        optimize_stmt_block(stmts_blk.take_statements(), state, true, true, false);
821                    *stmt = Stmt::Block(stmts_blk.into());
822                }
823                Expr::Stmt(..) => *stmt = Stmt::Noop(expr.position()),
824                _ => unreachable!("`Expr::Stmt`"),
825            }
826        }
827
828        // expr(func())
829        Stmt::Expr(expr) if matches!(**expr, Expr::FnCall(..)) => {
830            state.set_dirty();
831            match expr.take() {
832                Expr::FnCall(x, pos) => *stmt = Stmt::FnCall(x, pos),
833                _ => unreachable!(),
834            }
835        }
836
837        Stmt::Expr(expr) => optimize_expr(expr, state, false),
838
839        // func(...)
840        Stmt::FnCall(..) => match stmt.take() {
841            Stmt::FnCall(x, pos) => {
842                let mut expr = Expr::FnCall(x, pos);
843                optimize_expr(&mut expr, state, false);
844                *stmt = match expr {
845                    Expr::FnCall(x, pos) => Stmt::FnCall(x, pos),
846                    _ => Stmt::Expr(expr.into()),
847                }
848            }
849            _ => unreachable!(),
850        },
851
852        // break expr;
853        Stmt::BreakLoop(Some(ref mut expr), ..) => optimize_expr(expr, state, false),
854
855        // return expr;
856        Stmt::Return(Some(ref mut expr), ..) => optimize_expr(expr, state, false),
857
858        // Share nothing
859        #[cfg(not(feature = "no_closure"))]
860        Stmt::Share(x) if x.is_empty() => {
861            state.set_dirty();
862            *stmt = Stmt::Noop(Position::NONE);
863        }
864        // Share constants
865        #[cfg(not(feature = "no_closure"))]
866        Stmt::Share(x) => {
867            let orig_len = x.len();
868
869            if state.propagate_constants {
870                x.retain(|(v, _)| state.find_literal_constant(v.as_str()).is_none());
871
872                if x.len() != orig_len {
873                    state.set_dirty();
874                }
875            }
876        }
877
878        // All other statements - skip
879        _ => (),
880    }
881}
882
883/// Optimize an [expression][Expr].
884fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, _chaining: bool) {
885    // These keywords are handled specially
886    const DONT_EVAL_KEYWORDS: &[&str] = &[
887        KEYWORD_PRINT, // side effects
888        KEYWORD_DEBUG, // side effects
889        KEYWORD_EVAL,  // arbitrary scripts
890    ];
891
892    match expr {
893        // {}
894        Expr::Stmt(x) if x.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.position()) }
895        Expr::Stmt(x) if x.len() == 1 && matches!(x.statements()[0], Stmt::Expr(..)) => {
896            state.set_dirty();
897            match x.take_statements().remove(0) {
898                Stmt::Expr(mut e) => {
899                    optimize_expr(&mut e, state, false);
900                    *expr = *e;
901                }
902                _ => unreachable!("`Expr::Stmt`")
903            }
904        }
905        // { stmt; ... } - do not count promotion as dirty because it gets turned back into an array
906        Expr::Stmt(x) => {
907            *x.statements_mut() = optimize_stmt_block(x.take_statements(), state, true, true, false);
908
909            // { Stmt(Expr) } - promote
910            if let [ Stmt::Expr(e) ] = x.statements_mut().as_mut() { state.set_dirty(); *expr = e.take(); }
911        }
912        // ()?.rhs
913        #[cfg(not(feature = "no_object"))]
914        Expr::Dot(x, options, ..) if options.intersects(ASTFlags::NEGATED) && matches!(x.lhs, Expr::Unit(..)) => {
915            state.set_dirty();
916            *expr = x.lhs.take();
917        }
918        // lhs.rhs
919        #[cfg(not(feature = "no_object"))]
920        Expr::Dot(x, ..) if !_chaining => match (&mut x.lhs, &mut x.rhs) {
921            // map.string
922            (Expr::Map(m, pos), Expr::Property(p, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => {
923                // Map literal where everything is pure - promote the indexed item.
924                // All other items can be thrown away.
925                state.set_dirty();
926                *expr = mem::take(&mut m.0).into_iter().find(|(x, ..)| x.name == p.2)
927                            .map_or_else(|| Expr::Unit(*pos), |(.., mut expr)| { expr.set_position(*pos); expr });
928            }
929            // var.rhs or this.rhs
930            (Expr::Variable(..) | Expr::ThisPtr(..), rhs) => optimize_expr(rhs, state, true),
931            // const.type_of()
932            (lhs, Expr::MethodCall(x, pos)) if lhs.is_constant() && x.name == KEYWORD_TYPE_OF && x.args.is_empty() => {
933                if let Some(value) = lhs.get_literal_value() {
934                    state.set_dirty();
935                    let typ = state.engine.map_type_name(value.type_name()).into();
936                    *expr = Expr::from_dynamic(typ, *pos);
937                }
938            }
939            // const.is_shared()
940            #[cfg(not(feature = "no_closure"))]
941            (lhs, Expr::MethodCall(x, pos)) if lhs.is_constant() && x.name == crate::engine::KEYWORD_IS_SHARED && x.args.is_empty() => {
942                if lhs.get_literal_value().is_some() {
943                    state.set_dirty();
944                    *expr = Expr::from_dynamic(Dynamic::FALSE, *pos);
945                }
946            }
947            // lhs.rhs
948            (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, true); }
949        }
950        // ....lhs.rhs
951        #[cfg(not(feature = "no_object"))]
952        Expr::Dot(x,..) => { optimize_expr(&mut x.lhs, state, false); optimize_expr(&mut x.rhs, state, _chaining); }
953
954        // ()?[rhs]
955        #[cfg(not(feature = "no_index"))]
956        Expr::Index(x, options, ..) if options.intersects(ASTFlags::NEGATED) && matches!(x.lhs, Expr::Unit(..)) => {
957            state.set_dirty();
958            *expr = x.lhs.take();
959        }
960        // lhs[rhs]
961        #[cfg(not(feature = "no_index"))]
962        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
963        Expr::Index(x, ..) if !_chaining => match (&mut x.lhs, &mut x.rhs) {
964            // array[int]
965            (Expr::Array(a, pos), Expr::IntegerConstant(i, ..)) if *i >= 0 && *i <= crate::MAX_USIZE_INT && (*i as usize) < a.len() && a.iter().all(Expr::is_pure) => {
966                // Array literal where everything is pure - promote the indexed item.
967                // All other items can be thrown away.
968                state.set_dirty();
969                let mut result = a[*i as usize].take();
970                result.set_position(*pos);
971                *expr = result;
972            }
973            // array[-int]
974            #[allow(clippy::unnecessary_cast)]
975            (Expr::Array(a, pos), Expr::IntegerConstant(i, ..)) if *i < 0 && i.unsigned_abs() as u64 <= a.len() as u64 && a.iter().all(Expr::is_pure) => {
976                // Array literal where everything is pure - promote the indexed item.
977                // All other items can be thrown away.
978                state.set_dirty();
979                let index = a.len() - i.unsigned_abs() as usize;
980                let mut result = a[index].take();
981                result.set_position(*pos);
982                *expr = result;
983            }
984            // map[string]
985            (Expr::Map(m, pos), Expr::StringConstant(s, ..)) if m.0.iter().all(|(.., x)| x.is_pure()) => {
986                // Map literal where everything is pure - promote the indexed item.
987                // All other items can be thrown away.
988                state.set_dirty();
989                *expr = mem::take(&mut m.0).into_iter().find(|(x, ..)| x.name == s)
990                            .map_or_else(|| Expr::Unit(*pos), |(.., mut expr)| { expr.set_position(*pos); expr });
991            }
992            // int[int]
993            (Expr::IntegerConstant(n, pos), Expr::IntegerConstant(i, ..)) if *i >= 0 && *i <= crate::MAX_USIZE_INT && (*i as usize) < crate::INT_BITS => {
994                // Bit-field literal indexing - get the bit
995                state.set_dirty();
996                *expr = Expr::BoolConstant((*n & (1 << (*i as usize))) != 0, *pos);
997            }
998            // int[-int]
999            #[allow(clippy::unnecessary_cast)]
1000            (Expr::IntegerConstant(n, pos), Expr::IntegerConstant(i, ..)) if *i < 0 && i.unsigned_abs() as u64 <= crate::INT_BITS as u64 => {
1001                // Bit-field literal indexing - get the bit
1002                state.set_dirty();
1003                *expr = Expr::BoolConstant((*n & (1 << (crate::INT_BITS - i.unsigned_abs() as usize))) != 0, *pos);
1004            }
1005            // string[int]
1006            #[allow(clippy::unnecessary_cast)]
1007            (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, ..)) if *i >= 0 && *i <= crate::MAX_USIZE_INT && (*i as usize) < s.chars().count() => {
1008                // String literal indexing - get the character
1009                state.set_dirty();
1010                *expr = Expr::CharConstant(s.chars().nth(*i as usize).unwrap(), *pos);
1011            }
1012            // string[-int]
1013            #[allow(clippy::unnecessary_cast)]
1014            (Expr::StringConstant(s, pos), Expr::IntegerConstant(i, ..)) if *i < 0 && i.unsigned_abs() as u64 <= s.chars().count() as u64 => {
1015                // String literal indexing - get the character
1016                state.set_dirty();
1017                *expr = Expr::CharConstant(s.chars().rev().nth(i.unsigned_abs() as usize - 1).unwrap(), *pos);
1018            }
1019            // var[rhs] or this[rhs]
1020            (Expr::Variable(..) | Expr::ThisPtr(..), rhs) => optimize_expr(rhs, state, true),
1021            // lhs[rhs]
1022            (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, true); }
1023        },
1024        // ...[lhs][rhs]
1025        #[cfg(not(feature = "no_index"))]
1026        Expr::Index(x, ..) => { optimize_expr(&mut x.lhs, state, false); optimize_expr(&mut x.rhs, state, _chaining); }
1027        // ``
1028        Expr::InterpolatedString(x, pos) if x.is_empty() => {
1029            state.set_dirty();
1030            *expr = Expr::StringConstant(state.engine.const_empty_string(), *pos);
1031        }
1032        // `... ${const} ...`
1033        Expr::InterpolatedString(..) if expr.is_constant() => {
1034            state.set_dirty();
1035            *expr = Expr::StringConstant(expr.get_literal_value().unwrap().cast::<ImmutableString>(), expr.position());
1036        }
1037        // `... ${ ... } ...`
1038        Expr::InterpolatedString(x, ..) => {
1039            x.iter_mut().for_each(|expr| optimize_expr(expr, state, false));
1040
1041            let mut n = 0;
1042
1043            // Merge consecutive strings
1044            while n < x.len() - 1 {
1045                match (x[n].take(),x[n+1].take()) {
1046                    (Expr::StringConstant(mut s1, pos), Expr::StringConstant(s2, ..)) => { s1 += s2; x[n] = Expr::StringConstant(s1, pos); x.remove(n+1); state.set_dirty(); }
1047                    (expr1, Expr::Unit(..)) => { x[n] = expr1; x.remove(n+1); state.set_dirty(); }
1048                    (Expr::Unit(..), expr2) => { x[n+1] = expr2; x.remove(n); state.set_dirty(); }
1049                    (expr1, Expr::StringConstant(s, ..)) if s.is_empty() => { x[n] = expr1; x.remove(n+1); state.set_dirty(); }
1050                    (Expr::StringConstant(s, ..), expr2) if s.is_empty()=> { x[n+1] = expr2; x.remove(n); state.set_dirty(); }
1051                    (expr1, expr2) => { x[n] = expr1; x[n+1] = expr2; n += 1; }
1052                }
1053            }
1054
1055            x.shrink_to_fit();
1056        }
1057        // [ constant .. ]
1058        #[cfg(not(feature = "no_index"))]
1059        Expr::Array(..) if expr.is_constant() => {
1060            state.set_dirty();
1061            *expr = Expr::DynamicConstant(expr.get_literal_value().unwrap().into(), expr.position());
1062        }
1063        // [ items .. ]
1064        #[cfg(not(feature = "no_index"))]
1065        Expr::Array(x, ..) => x.iter_mut().for_each(|expr| optimize_expr(expr, state, false)),
1066        // #{ key:constant, .. }
1067        #[cfg(not(feature = "no_object"))]
1068        Expr::Map(..) if expr.is_constant() => {
1069            state.set_dirty();
1070            *expr = Expr::DynamicConstant(expr.get_literal_value().unwrap().into(), expr.position());
1071        }
1072        // #{ key:value, .. }
1073        #[cfg(not(feature = "no_object"))]
1074        Expr::Map(x, ..) => x.0.iter_mut().for_each(|(.., expr)| optimize_expr(expr, state, false)),
1075        // lhs && rhs
1076        Expr::And(x, ..) => match (&mut x.lhs, &mut x.rhs) {
1077            // true && rhs -> rhs
1078            (Expr::BoolConstant(true, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = rhs.take(); }
1079            // false && rhs -> false
1080            (Expr::BoolConstant(false, pos), ..) => { state.set_dirty(); *expr = Expr::BoolConstant(false, *pos); }
1081            // lhs && true -> lhs
1082            (lhs, Expr::BoolConstant(true, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = lhs.take(); }
1083            // lhs && rhs
1084            (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); }
1085        },
1086        // lhs || rhs
1087        Expr::Or(ref mut x, ..) => match (&mut x.lhs, &mut x.rhs) {
1088            // false || rhs -> rhs
1089            (Expr::BoolConstant(false, ..), rhs) => { state.set_dirty(); optimize_expr(rhs, state, false); *expr = rhs.take(); }
1090            // true || rhs -> true
1091            (Expr::BoolConstant(true, pos), ..) => { state.set_dirty(); *expr = Expr::BoolConstant(true, *pos); }
1092            // lhs || false
1093            (lhs, Expr::BoolConstant(false, ..)) => { state.set_dirty(); optimize_expr(lhs, state, false); *expr = lhs.take(); }
1094            // lhs || rhs
1095            (lhs, rhs) => { optimize_expr(lhs, state, false); optimize_expr(rhs, state, false); }
1096        },
1097        // () ?? rhs -> rhs
1098        Expr::Coalesce(x, ..) if matches!(x.lhs, Expr::Unit(..)) => {
1099            state.set_dirty();
1100            *expr = x.rhs.take();
1101        },
1102        // lhs:constant ?? rhs -> lhs
1103        Expr::Coalesce(x, ..) if x.lhs.is_constant() => {
1104            state.set_dirty();
1105            *expr = x.lhs.take();
1106        },
1107
1108        // !true or !false
1109        Expr::FnCall(x,..)
1110            if x.name == OP_NOT
1111            && x.args.len() == 1
1112            && matches!(x.args[0], Expr::BoolConstant(..))
1113        => {
1114            state.set_dirty();
1115            match x.args[0] {
1116                Expr::BoolConstant(b, pos) => *expr = Expr::BoolConstant(!b, pos),
1117                _ => unreachable!(),
1118            }
1119        }
1120
1121        // eval!
1122        Expr::FnCall(x, ..) if x.name == KEYWORD_EVAL => {
1123            state.propagate_constants = false;
1124        }
1125        // Fn
1126        Expr::FnCall(x, pos)
1127            if !x.is_qualified() // Non-qualified
1128            && x.args.len() == 1
1129            && x.name == KEYWORD_FN_PTR
1130            && x.constant_args()
1131        => {
1132            let fn_name = match x.args[0] {
1133                Expr::StringConstant(ref s, ..) => s.clone().into(),
1134                _ => Dynamic::UNIT
1135            };
1136
1137            if let Ok(fn_ptr) = fn_name.into_immutable_string().map_err(Into::into).and_then(FnPtr::try_from) {
1138                state.set_dirty();
1139                *expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), *pos);
1140            } else {
1141                optimize_expr(&mut x.args[0], state, false);
1142            }
1143        }
1144        // curry(FnPtr, constants...)
1145        Expr::FnCall(x, pos)
1146            if !x.is_qualified() // Non-qualified
1147            && x.args.len() >= 2
1148            && x.name == KEYWORD_FN_PTR_CURRY
1149            && matches!(x.args[0], Expr::DynamicConstant(ref v, ..) if v.is_fnptr())
1150            && x.constant_args()
1151        => {
1152            let mut fn_ptr = x.args[0].get_literal_value().unwrap().cast::<FnPtr>();
1153            fn_ptr.extend(x.args.iter().skip(1).map(|arg_expr| arg_expr.get_literal_value().unwrap()));
1154            state.set_dirty();
1155            *expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), *pos);
1156        }
1157
1158        // Do not call some special keywords that may have side effects
1159        Expr::FnCall(x, ..) if DONT_EVAL_KEYWORDS.contains(&x.name.as_str()) => {
1160            x.args.iter_mut().for_each(|arg_expr| optimize_expr(arg_expr, state, false));
1161        }
1162
1163        // Call built-in operators
1164        Expr::FnCall(x, pos)
1165                if !x.is_qualified() // Non-qualified
1166                && state.optimization_level == OptimizationLevel::Simple // simple optimizations
1167                && x.constant_args() // all arguments are constants
1168        => {
1169            let arg_values = &mut x.args.iter().map(|arg_expr| arg_expr.get_literal_value().unwrap()).collect::<FnArgsVec<_>>();
1170            let arg_types = arg_values.iter().map(Dynamic::type_id).collect::<FnArgsVec<_>>();
1171
1172            match x.name.as_str() {
1173                KEYWORD_TYPE_OF if arg_values.len() == 1 => {
1174                    state.set_dirty();
1175                    let typ = state.engine.map_type_name(arg_values[0].type_name()).into();
1176                    *expr = Expr::from_dynamic(typ, *pos);
1177                    return;
1178                }
1179                #[cfg(not(feature = "no_closure"))]
1180                crate::engine::KEYWORD_IS_SHARED if arg_values.len() == 1 => {
1181                    state.set_dirty();
1182                    *expr = Expr::from_dynamic(Dynamic::FALSE, *pos);
1183                    return;
1184                }
1185                // Overloaded operators can override built-in.
1186                _ if x.args.len() == 2 && x.op_token.is_some() && (state.engine.fast_operators() || !state.engine.has_native_fn_override(x.hashes.native(), &arg_types)) => {
1187                    if let Some((f, ctx)) = get_builtin_binary_op_fn(x.op_token.as_ref().unwrap(), &arg_values[0], &arg_values[1]) {
1188                        let context = ctx.then(|| (state.engine, x.name.as_str(), None, &state.global, *pos).into());
1189                        let (first, second) = arg_values.split_first_mut().unwrap();
1190
1191                        if let Ok(result) = f(context, &mut [ first, &mut second[0] ]) {
1192                            state.set_dirty();
1193                            *expr = Expr::from_dynamic(result, *pos);
1194                            return;
1195                        }
1196                    }
1197                }
1198                _ => ()
1199            }
1200
1201            x.args.iter_mut().for_each(|arg_expr| optimize_expr(arg_expr, state, false));
1202
1203            // Move constant arguments
1204            x.args.iter_mut().for_each(|arg_expr| match arg_expr {
1205                    Expr::DynamicConstant(..) | Expr::Unit(..)
1206                    | Expr::StringConstant(..) | Expr::CharConstant(..)
1207                    | Expr::BoolConstant(..) | Expr::IntegerConstant(..) => (),
1208
1209                    #[cfg(not(feature = "no_float"))]
1210                    Expr:: FloatConstant(..) => (),
1211
1212                    _ => if let Some(value) = arg_expr.get_literal_value() {
1213                        state.set_dirty();
1214                        *arg_expr = Expr::DynamicConstant(value.into(), arg_expr.start_position());
1215                    },
1216                });
1217        }
1218
1219        // Eagerly call functions
1220        Expr::FnCall(x, pos)
1221                if !x.is_qualified() // non-qualified
1222                && state.optimization_level == OptimizationLevel::Full // full optimizations
1223                && x.constant_args() // all arguments are constants
1224        => {
1225            // First search for script-defined functions (can override built-in)
1226            let _has_script_fn = false;
1227            #[cfg(not(feature = "no_function"))]
1228            let _has_script_fn = !x.hashes.is_native_only() && state.global.lib.iter().find_map(|m| m.get_script_fn(&x.name, x.args.len())).is_some();
1229
1230            if !_has_script_fn {
1231                let arg_values = &mut x.args.iter().map(Expr::get_literal_value).collect::<Option<FnArgsVec<_>>>().unwrap();
1232
1233                let result = match x.name.as_str() {
1234                    KEYWORD_TYPE_OF if arg_values.len() == 1 => Some(state.engine.map_type_name(arg_values[0].type_name()).into()),
1235                    #[cfg(not(feature = "no_closure"))]
1236                    crate::engine::KEYWORD_IS_SHARED if arg_values.len() == 1 => Some(Dynamic::FALSE),
1237                    _ => state.call_fn_with_const_args(&x.name, x.op_token.as_ref(), arg_values)
1238                };
1239
1240                if let Some(r) = result {
1241                    state.set_dirty();
1242                    *expr = Expr::from_dynamic(r, *pos);
1243                    return;
1244                }
1245            }
1246
1247            x.args.iter_mut().for_each(|a| optimize_expr(a, state, false));
1248        }
1249
1250        // id(args ..) or xxx.id(args ..) -> optimize function call arguments
1251        Expr::FnCall(x, ..) | Expr::MethodCall(x, ..) => x.args.iter_mut().for_each(|arg_expr| {
1252            optimize_expr(arg_expr, state, false);
1253
1254            // Move constant arguments
1255            match arg_expr {
1256                Expr::DynamicConstant(..) | Expr::Unit(..)
1257                | Expr::StringConstant(..) | Expr::CharConstant(..)
1258                | Expr::BoolConstant(..) | Expr::IntegerConstant(..) => (),
1259
1260                #[cfg(not(feature = "no_float"))]
1261                Expr:: FloatConstant(..) => (),
1262
1263                _ => if let Some(value) = arg_expr.get_literal_value() {
1264                    state.set_dirty();
1265                    *arg_expr = Expr::DynamicConstant(value.into(), arg_expr.start_position());
1266                },
1267            }
1268        }),
1269
1270        // constant-name
1271        #[cfg(not(feature = "no_module"))]
1272        Expr::Variable(x, ..) if !x.1.is_empty() => (),
1273        Expr::Variable(x, .., pos) if state.propagate_constants && state.find_literal_constant(&x.3).is_some() => {
1274            // Replace constant with value
1275            *expr = Expr::from_dynamic(state.find_literal_constant(&x.3).unwrap().clone(), *pos);
1276            state.set_dirty();
1277        }
1278
1279        // Custom syntax
1280        #[cfg(not(feature = "no_custom_syntax"))]
1281        Expr::Custom(x, ..) => {
1282            if x.scope_may_be_changed {
1283                state.propagate_constants = false;
1284            }
1285            // Do not optimize custom syntax expressions as you won't know how they would be called
1286        }
1287
1288        // All other expressions - skip
1289        _ => (),
1290    }
1291}
1292
1293impl Engine {
1294    /// Has a system function a Rust-native override?
1295    fn has_native_fn_override(&self, hash_script: u64, arg_types: impl AsRef<[TypeId]>) -> bool {
1296        let hash = calc_fn_hash_full(hash_script, arg_types.as_ref().iter().copied());
1297
1298        // First check the global namespace and packages, but skip modules that are standard because
1299        // they should never conflict with system functions.
1300        if self
1301            .global_modules
1302            .iter()
1303            .filter(|m| !m.flags.intersects(ModuleFlags::STANDARD_LIB))
1304            .any(|m| m.contains_fn(hash))
1305        {
1306            return true;
1307        }
1308
1309        // Then check sub-modules
1310        #[cfg(not(feature = "no_module"))]
1311        if self
1312            .global_sub_modules
1313            .values()
1314            .any(|m| m.contains_qualified_fn(hash))
1315        {
1316            return true;
1317        }
1318
1319        false
1320    }
1321
1322    /// Optimize a block of [statements][Stmt] at top level.
1323    ///
1324    /// Constants and variables from the scope are added.
1325    fn optimize_top_level(
1326        &self,
1327        statements: StmtBlockContainer,
1328        scope: Option<&Scope>,
1329        lib: &[crate::SharedModule],
1330        optimization_level: OptimizationLevel,
1331    ) -> StmtBlockContainer {
1332        let mut statements = statements;
1333
1334        // If optimization level is None then skip optimizing
1335        if optimization_level == OptimizationLevel::None {
1336            statements.shrink_to_fit();
1337            return statements;
1338        }
1339
1340        // Set up the state
1341        let mut state = OptimizerState::new(self, lib, scope, optimization_level);
1342
1343        // Add constants from global modules
1344        self.global_modules
1345            .iter()
1346            .rev()
1347            .flat_map(|m| m.iter_var())
1348            .for_each(|(name, value)| state.push_var(name.into(), Some(Cow::Borrowed(value))));
1349
1350        // Add constants and variables from the scope
1351        state
1352            .scope
1353            .into_iter()
1354            .flat_map(Scope::iter_inner)
1355            .for_each(|(name, constant, value)| {
1356                state.push_var(
1357                    name.into(),
1358                    if constant {
1359                        Some(Cow::Borrowed(value))
1360                    } else {
1361                        None
1362                    },
1363                );
1364            });
1365
1366        optimize_stmt_block(statements, &mut state, true, false, true)
1367    }
1368}
1369
1370/// Optimize a collection of statements and functions into an [`AST`].
1371pub fn optimize_into_ast(
1372    engine: &Engine,
1373    scope: Option<&Scope>,
1374    statements: StmtBlockContainer,
1375    #[cfg(not(feature = "no_function"))] functions: Vec<crate::Shared<crate::ast::ScriptFuncDef>>,
1376    optimization_level: OptimizationLevel,
1377) -> AST {
1378    let mut statements = statements;
1379
1380    #[cfg(not(feature = "no_function"))]
1381    let lib: crate::Shared<_> = {
1382        let mut module = crate::Module::new();
1383
1384        if optimization_level == OptimizationLevel::None {
1385            for fn_def in functions {
1386                module.set_script_fn(fn_def);
1387            }
1388        } else {
1389            // We only need the script library's signatures for optimization purposes
1390            let mut lib2 = crate::Module::new();
1391
1392            functions
1393                .iter()
1394                .map(|fn_def| crate::ast::ScriptFuncDef {
1395                    name: fn_def.name.clone(),
1396                    access: fn_def.access,
1397                    body: crate::ast::StmtBlock::NONE,
1398                    #[cfg(not(feature = "no_object"))]
1399                    this_type: fn_def.this_type.clone(),
1400                    params: fn_def.params.clone(),
1401                    #[cfg(feature = "metadata")]
1402                    comments: <_>::default(),
1403                })
1404                .for_each(|script_def| {
1405                    lib2.set_script_fn(script_def);
1406                });
1407
1408            let lib2 = &[lib2.into()];
1409
1410            for fn_def in functions {
1411                let mut fn_def = crate::func::shared_take_or_clone(fn_def);
1412
1413                // Optimize the function body
1414                *fn_def.body.statements_mut() = engine.optimize_top_level(
1415                    fn_def.body.take_statements(),
1416                    scope,
1417                    lib2,
1418                    optimization_level,
1419                );
1420
1421                module.set_script_fn(fn_def);
1422            }
1423        }
1424
1425        module.into()
1426    };
1427    #[cfg(feature = "no_function")]
1428    let lib: crate::Shared<_> = crate::Module::new().into();
1429
1430    statements.shrink_to_fit();
1431
1432    AST::new(
1433        match optimization_level {
1434            OptimizationLevel::None => statements,
1435            OptimizationLevel::Simple | OptimizationLevel::Full => {
1436                engine.optimize_top_level(statements, scope, &[lib.clone()], optimization_level)
1437            }
1438        },
1439        #[cfg(not(feature = "no_function"))]
1440        lib,
1441    )
1442}