Skip to main content

nwnrs_nwscript/
opt.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    BinaryOp, BuiltinValue, HirBlock, HirExpr, HirExprKind, HirFunction, HirIfStmt, HirModule,
5    HirStmt, HirSwitchStmt, LangSpec, Literal, NcsInstruction, NcsOpcode, OptimizationLevel,
6    SemanticType, UnaryOp,
7};
8
9pub(crate) fn optimize_hir(
10    hir: &HirModule,
11    langspec: Option<&LangSpec>,
12    optimization: OptimizationLevel,
13) -> HirModule {
14    let mut optimized = hir.clone();
15
16    if enables_dead_branches(optimization) {
17        optimized = trim_dead_branches(&optimized, langspec);
18    }
19
20    if enables_dead_functions(optimization) {
21        optimized = eliminate_dead_functions(&optimized);
22    }
23
24    optimized
25}
26
27pub(crate) fn meld_instructions(instructions: Vec<NcsInstruction>) -> Vec<NcsInstruction> {
28    let mut optimized: Vec<NcsInstruction> = Vec::with_capacity(instructions.len());
29
30    for instruction in instructions {
31        if instruction.opcode == NcsOpcode::ModifyStackPointer
32            && let [.., runstack_add, constant, assignment] = optimized.as_slice()
33            && runstack_add.opcode == NcsOpcode::RunstackAdd
34            && constant.opcode == NcsOpcode::Constant
35            && assignment.opcode == NcsOpcode::Assignment
36            && runstack_add.auxcode == constant.auxcode
37            && assignment_stack_offset(assignment) == Some(-8)
38        {
39            let constant = constant.clone();
40            optimized.pop();
41            optimized.pop();
42            optimized.pop();
43            optimized.push(constant);
44            continue;
45        }
46
47        optimized.push(instruction);
48    }
49
50    optimized
51}
52
53fn enables_dead_functions(optimization: OptimizationLevel) -> bool {
54    #[allow(non_exhaustive_omitted_patterns)] match optimization {
    OptimizationLevel::O1 | OptimizationLevel::O2 | OptimizationLevel::O3 =>
        true,
    _ => false,
}matches!(
55        optimization,
56        OptimizationLevel::O1 | OptimizationLevel::O2 | OptimizationLevel::O3
57    )
58}
59
60fn enables_dead_branches(optimization: OptimizationLevel) -> bool {
61    #[allow(non_exhaustive_omitted_patterns)] match optimization {
    OptimizationLevel::O2 | OptimizationLevel::O3 => true,
    _ => false,
}matches!(optimization, OptimizationLevel::O2 | OptimizationLevel::O3)
62}
63
64fn enables_instruction_melding(optimization: OptimizationLevel) -> bool {
65    optimization == OptimizationLevel::O3
66}
67
68pub(crate) fn optimization_needs_post_codegen_passes(optimization: OptimizationLevel) -> bool {
69    enables_instruction_melding(optimization)
70}
71
72pub(crate) fn optimization_needs_hir_passes(optimization: OptimizationLevel) -> bool {
73    enables_dead_functions(optimization) || enables_dead_branches(optimization)
74}
75
76#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ConstValue {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            ConstValue::Int(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Int",
                    &__self_0),
            ConstValue::Float(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Float",
                    &__self_0),
            ConstValue::String(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "String",
                    &__self_0),
        }
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for ConstValue {
    #[inline]
    fn clone(&self) -> ConstValue {
        match self {
            ConstValue::Int(__self_0) =>
                ConstValue::Int(::core::clone::Clone::clone(__self_0)),
            ConstValue::Float(__self_0) =>
                ConstValue::Float(::core::clone::Clone::clone(__self_0)),
            ConstValue::String(__self_0) =>
                ConstValue::String(::core::clone::Clone::clone(__self_0)),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for ConstValue {
    #[inline]
    fn eq(&self, other: &ConstValue) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr &&
            match (self, other) {
                (ConstValue::Int(__self_0), ConstValue::Int(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (ConstValue::Float(__self_0), ConstValue::Float(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (ConstValue::String(__self_0), ConstValue::String(__arg1_0))
                    => __self_0 == __arg1_0,
                _ => unsafe { ::core::intrinsics::unreachable() }
            }
    }
}PartialEq)]
77pub(crate) enum ConstValue {
78    Int(i32),
79    Float(f32),
80    String(String),
81}
82
83fn trim_dead_branches(hir: &HirModule, langspec: Option<&LangSpec>) -> HirModule {
84    let constants = build_constant_env(hir, langspec);
85    let mut optimized = hir.clone();
86    for function in &mut optimized.functions {
87        if let Some(body) = function.body.take() {
88            function.body = Some(optimize_block(body, &constants));
89        }
90    }
91    optimized
92}
93
94fn optimize_block(block: HirBlock, constants: &BTreeMap<String, ConstValue>) -> HirBlock {
95    let mut statements = Vec::with_capacity(block.statements.len());
96    for statement in block.statements {
97        if let Some(statement) = optimize_stmt(statement, constants) {
98            statements.push(statement);
99        }
100    }
101    HirBlock {
102        span: block.span,
103        statements,
104    }
105}
106
107fn optimize_stmt(statement: HirStmt, constants: &BTreeMap<String, ConstValue>) -> Option<HirStmt> {
108    match statement {
109        HirStmt::Block(block) => Some(HirStmt::Block(Box::new(optimize_block(*block, constants)))),
110        HirStmt::If(boxed) => {
111            let HirIfStmt {
112                span,
113                condition,
114                then_branch,
115                else_branch,
116            } = *boxed;
117            if let Some(ConstValue::Int(value)) = evaluate_const_expr(&condition, constants) {
118                if value != 0 {
119                    return optimize_stmt(*then_branch, constants);
120                }
121                return else_branch.and_then(|branch| optimize_stmt(*branch, constants));
122            }
123
124            Some(HirStmt::If(Box::new(HirIfStmt {
125                span,
126                condition,
127                then_branch: Box::new(
128                    optimize_stmt(*then_branch, constants).unwrap_or(HirStmt::Empty(span)),
129                ),
130                else_branch: else_branch
131                    .and_then(|branch| optimize_stmt(*branch, constants))
132                    .map(Box::new),
133            })))
134        }
135        HirStmt::Switch(boxed) => {
136            let HirSwitchStmt {
137                span,
138                condition,
139                body,
140            } = *boxed;
141            Some(HirStmt::Switch(Box::new(HirSwitchStmt {
142                span,
143                condition,
144                body: Box::new(optimize_stmt(*body, constants).unwrap_or(HirStmt::Empty(span))),
145            })))
146        }
147        HirStmt::While(mut statement) => {
148            statement.body = Box::new(
149                optimize_stmt(*statement.body, constants).unwrap_or(HirStmt::Empty(statement.span)),
150            );
151            Some(HirStmt::While(statement))
152        }
153        HirStmt::DoWhile(mut statement) => {
154            statement.body = Box::new(
155                optimize_stmt(*statement.body, constants).unwrap_or(HirStmt::Empty(statement.span)),
156            );
157            Some(HirStmt::DoWhile(statement))
158        }
159        HirStmt::For(mut statement) => {
160            statement.body = Box::new(
161                optimize_stmt(*statement.body, constants).unwrap_or(HirStmt::Empty(statement.span)),
162            );
163            Some(HirStmt::For(statement))
164        }
165        other => Some(other),
166    }
167}
168
169fn eliminate_dead_functions(hir: &HirModule) -> HirModule {
170    let function_map = hir
171        .functions
172        .iter()
173        .enumerate()
174        .map(|(index, function)| (function.name.clone(), (index, function)))
175        .collect::<BTreeMap<_, _>>();
176
177    let entry_name = function_map.get("main").map(|_| "main").or_else(|| {
178        function_map
179            .get("StartingConditional")
180            .map(|_| "StartingConditional")
181    });
182
183    let mut visited = BTreeSet::new();
184    let mut ordered = Vec::new();
185
186    if !hir.globals.is_empty() {
187        for global in &hir.globals {
188            if let Some(initializer) = &global.initializer {
189                collect_user_calls_expr(initializer, &mut ordered, &mut visited, &function_map);
190            }
191        }
192    }
193
194    if let Some(entry_name) = entry_name {
195        visit_function(entry_name, &mut ordered, &mut visited, &function_map);
196    }
197
198    let mut functions = Vec::with_capacity(ordered.len());
199    for name in ordered {
200        if let Some((_, function)) = function_map.get(&name) {
201            functions.push((*function).clone());
202        }
203    }
204
205    HirModule {
206        includes: hir.includes.clone(),
207        structs: hir.structs.clone(),
208        globals: hir.globals.clone(),
209        functions,
210    }
211}
212
213fn visit_function(
214    name: &str,
215    ordered: &mut Vec<String>,
216    visited: &mut BTreeSet<String>,
217    function_map: &BTreeMap<String, (usize, &HirFunction)>,
218) {
219    if !visited.insert(name.to_string()) {
220        return;
221    }
222
223    let Some((_, function)) = function_map.get(name) else {
224        return;
225    };
226    ordered.push(name.to_string());
227
228    if let Some(body) = &function.body {
229        collect_user_calls_block(body, ordered, visited, function_map);
230    }
231}
232
233fn collect_user_calls_block(
234    block: &HirBlock,
235    ordered: &mut Vec<String>,
236    visited: &mut BTreeSet<String>,
237    function_map: &BTreeMap<String, (usize, &HirFunction)>,
238) {
239    for statement in &block.statements {
240        collect_user_calls_stmt(statement, ordered, visited, function_map);
241    }
242}
243
244fn collect_user_calls_stmt(
245    statement: &HirStmt,
246    ordered: &mut Vec<String>,
247    visited: &mut BTreeSet<String>,
248    function_map: &BTreeMap<String, (usize, &HirFunction)>,
249) {
250    match statement {
251        HirStmt::Block(block) => collect_user_calls_block(block, ordered, visited, function_map),
252        HirStmt::Declare(statement) => {
253            for declarator in &statement.declarators {
254                if let Some(initializer) = &declarator.initializer {
255                    collect_user_calls_expr(initializer, ordered, visited, function_map);
256                }
257            }
258        }
259        HirStmt::Expr(expr) | HirStmt::Case(expr) => {
260            collect_user_calls_expr(expr, ordered, visited, function_map);
261        }
262        HirStmt::If(statement) => {
263            collect_user_calls_expr(&statement.condition, ordered, visited, function_map);
264            collect_user_calls_stmt(&statement.then_branch, ordered, visited, function_map);
265            if let Some(else_branch) = &statement.else_branch {
266                collect_user_calls_stmt(else_branch, ordered, visited, function_map);
267            }
268        }
269        HirStmt::Switch(statement) => {
270            collect_user_calls_expr(&statement.condition, ordered, visited, function_map);
271            collect_user_calls_stmt(&statement.body, ordered, visited, function_map);
272        }
273        HirStmt::Return(statement) => {
274            if let Some(value) = &statement.value {
275                collect_user_calls_expr(value, ordered, visited, function_map);
276            }
277        }
278        HirStmt::While(statement) => {
279            collect_user_calls_expr(&statement.condition, ordered, visited, function_map);
280            collect_user_calls_stmt(&statement.body, ordered, visited, function_map);
281        }
282        HirStmt::DoWhile(statement) => {
283            collect_user_calls_stmt(&statement.body, ordered, visited, function_map);
284            collect_user_calls_expr(&statement.condition, ordered, visited, function_map);
285        }
286        HirStmt::For(statement) => {
287            if let Some(initializer) = &statement.initializer {
288                collect_user_calls_expr(initializer, ordered, visited, function_map);
289            }
290            if let Some(condition) = &statement.condition {
291                collect_user_calls_expr(condition, ordered, visited, function_map);
292            }
293            collect_user_calls_stmt(&statement.body, ordered, visited, function_map);
294            if let Some(update) = &statement.update {
295                collect_user_calls_expr(update, ordered, visited, function_map);
296            }
297        }
298        HirStmt::Default(_) | HirStmt::Break(_) | HirStmt::Continue(_) | HirStmt::Empty(_) => {}
299    }
300}
301
302fn collect_user_calls_expr(
303    expr: &HirExpr,
304    ordered: &mut Vec<String>,
305    visited: &mut BTreeSet<String>,
306    function_map: &BTreeMap<String, (usize, &HirFunction)>,
307) {
308    match &expr.kind {
309        HirExprKind::Literal(_) | HirExprKind::Value(_) => {}
310        HirExprKind::Call {
311            target,
312            arguments,
313        } => {
314            for argument in arguments {
315                collect_user_calls_expr(argument, ordered, visited, function_map);
316            }
317            if let crate::HirCallTarget::Function(name) = target {
318                visit_function(name, ordered, visited, function_map);
319            }
320        }
321        HirExprKind::FieldAccess {
322            base, ..
323        } => {
324            collect_user_calls_expr(base, ordered, visited, function_map);
325        }
326        HirExprKind::Unary {
327            expr, ..
328        } => {
329            collect_user_calls_expr(expr, ordered, visited, function_map);
330        }
331        HirExprKind::Binary {
332            left,
333            right,
334            ..
335        } => {
336            collect_user_calls_expr(left, ordered, visited, function_map);
337            collect_user_calls_expr(right, ordered, visited, function_map);
338        }
339        HirExprKind::Conditional {
340            condition,
341            when_true,
342            when_false,
343        } => {
344            collect_user_calls_expr(condition, ordered, visited, function_map);
345            collect_user_calls_expr(when_true, ordered, visited, function_map);
346            collect_user_calls_expr(when_false, ordered, visited, function_map);
347        }
348        HirExprKind::Assignment {
349            op,
350            left,
351            right,
352        } => {
353            if *op != crate::AssignmentOp::Assign {
354                collect_user_calls_expr(left, ordered, visited, function_map);
355            }
356            collect_user_calls_expr(right, ordered, visited, function_map);
357        }
358    }
359}
360
361pub(crate) fn build_constant_env(
362    hir: &HirModule,
363    langspec: Option<&LangSpec>,
364) -> BTreeMap<String, ConstValue> {
365    let mut constants = BTreeMap::new();
366    if let Some(langspec) = langspec {
367        for constant in &langspec.constants {
368            if let Some(value) = const_from_builtin_value(&constant.value) {
369                constants.insert(constant.name.clone(), value);
370            }
371        }
372    }
373
374    for global in &hir.globals {
375        if !global.is_const {
376            continue;
377        }
378        let value = global
379            .initializer
380            .as_ref()
381            .and_then(|initializer| evaluate_const_expr(initializer, &constants))
382            .or_else(|| default_const_value(&global.ty));
383        if let Some(value) = value {
384            constants.insert(global.name.clone(), value);
385        }
386    }
387
388    constants
389}
390
391pub(crate) fn evaluate_const_expr(
392    expr: &HirExpr,
393    constants: &BTreeMap<String, ConstValue>,
394) -> Option<ConstValue> {
395    match &expr.kind {
396        HirExprKind::Literal(literal) => const_from_literal(literal),
397        HirExprKind::Value(
398            crate::HirValueRef::ConstGlobal(name) | crate::HirValueRef::BuiltinConstant(name),
399        ) => constants.get(name).cloned(),
400        HirExprKind::Unary {
401            op,
402            expr,
403        } => {
404            let value = evaluate_const_expr(expr, constants)?;
405            match (op, value) {
406                (UnaryOp::Negate, ConstValue::Int(value)) => Some(ConstValue::Int(-value)),
407                (UnaryOp::Negate, ConstValue::Float(value)) => Some(ConstValue::Float(-value)),
408                (UnaryOp::BooleanNot, ConstValue::Int(value)) => {
409                    Some(ConstValue::Int(i32::from(value == 0)))
410                }
411                (UnaryOp::OnesComplement, ConstValue::Int(value)) => Some(ConstValue::Int(!value)),
412                _ => None,
413            }
414        }
415        HirExprKind::Binary {
416            op,
417            left,
418            right,
419        } => {
420            let left = evaluate_const_expr(left, constants)?;
421            let right = evaluate_const_expr(right, constants)?;
422            match (left, right) {
423                (ConstValue::Int(left), ConstValue::Int(right)) => {
424                    evaluate_int_binary(*op, left, right).map(ConstValue::Int)
425                }
426                (ConstValue::Float(left), ConstValue::Float(right)) => {
427                    evaluate_float_binary(*op, left, right)
428                }
429                (ConstValue::String(left), ConstValue::String(right)) => {
430                    evaluate_string_binary(*op, &left, &right)
431                }
432                _ => None,
433            }
434        }
435        HirExprKind::Conditional {
436            condition,
437            when_true,
438            when_false,
439        } => match evaluate_const_expr(condition, constants)? {
440            ConstValue::Int(value) if value != 0 => evaluate_const_expr(when_true, constants),
441            ConstValue::Int(_) => evaluate_const_expr(when_false, constants),
442            _ => None,
443        },
444        HirExprKind::Value(_)
445        | HirExprKind::Call {
446            ..
447        }
448        | HirExprKind::FieldAccess {
449            ..
450        }
451        | HirExprKind::Assignment {
452            ..
453        } => None,
454    }
455}
456
457fn const_from_builtin_value(value: &BuiltinValue) -> Option<ConstValue> {
458    match value {
459        BuiltinValue::Int(value) | BuiltinValue::ObjectId(value) => Some(ConstValue::Int(*value)),
460        BuiltinValue::Float(value) => Some(ConstValue::Float(*value)),
461        BuiltinValue::String(value) => Some(ConstValue::String(value.clone())),
462        BuiltinValue::ObjectSelf => Some(ConstValue::Int(0)),
463        BuiltinValue::ObjectInvalid => Some(ConstValue::Int(1)),
464        BuiltinValue::LocationInvalid
465        | BuiltinValue::Json(_)
466        | BuiltinValue::Vector(_)
467        | BuiltinValue::Raw(_) => None,
468    }
469}
470
471fn const_from_literal(literal: &Literal) -> Option<ConstValue> {
472    match literal {
473        Literal::Integer(value) => Some(ConstValue::Int(*value)),
474        Literal::Float(value) => Some(ConstValue::Float(*value)),
475        Literal::String(value) => Some(ConstValue::String(value.clone())),
476        Literal::ObjectSelf => Some(ConstValue::Int(0)),
477        Literal::ObjectInvalid => Some(ConstValue::Int(1)),
478        Literal::LocationInvalid | Literal::Json(_) | Literal::Vector(_) | Literal::Magic(_) => {
479            None
480        }
481    }
482}
483
484fn default_const_value(ty: &SemanticType) -> Option<ConstValue> {
485    match ty {
486        SemanticType::Int => Some(ConstValue::Int(0)),
487        SemanticType::Float => Some(ConstValue::Float(0.0)),
488        SemanticType::String => Some(ConstValue::String(String::new())),
489        _ => None,
490    }
491}
492
493fn evaluate_int_binary(op: BinaryOp, left: i32, right: i32) -> Option<i32> {
494    match op {
495        BinaryOp::Multiply => Some(left.wrapping_mul(right)),
496        BinaryOp::Divide => (right != 0).then_some(left.wrapping_div(right)),
497        BinaryOp::Modulus => (right != 0).then_some(left.wrapping_rem(right)),
498        BinaryOp::Add => Some(left.wrapping_add(right)),
499        BinaryOp::Subtract => Some(left.wrapping_sub(right)),
500        BinaryOp::ShiftLeft => Some(left.wrapping_shl(right.cast_unsigned())),
501        BinaryOp::ShiftRight => Some(left.wrapping_shr(right.cast_unsigned())),
502        BinaryOp::UnsignedShiftRight => {
503            Some(((left.cast_unsigned()).wrapping_shr(right.cast_unsigned())).cast_signed())
504        }
505        BinaryOp::GreaterEqual => Some(i32::from(left >= right)),
506        BinaryOp::GreaterThan => Some(i32::from(left > right)),
507        BinaryOp::LessThan => Some(i32::from(left < right)),
508        BinaryOp::LessEqual => Some(i32::from(left <= right)),
509        BinaryOp::NotEqual => Some(i32::from(left != right)),
510        BinaryOp::EqualEqual => Some(i32::from(left == right)),
511        BinaryOp::BooleanAnd => Some(left & right),
512        BinaryOp::ExclusiveOr => Some(left ^ right),
513        BinaryOp::InclusiveOr => Some(left | right),
514        BinaryOp::LogicalAnd => Some(i32::from((left != 0) && (right != 0))),
515        BinaryOp::LogicalOr => Some(i32::from((left != 0) || (right != 0))),
516    }
517}
518
519#[allow(clippy::float_cmp)]
520fn evaluate_float_binary(op: BinaryOp, left: f32, right: f32) -> Option<ConstValue> {
521    match op {
522        BinaryOp::Multiply => Some(ConstValue::Float(left * right)),
523        BinaryOp::Divide => Some(ConstValue::Float(left / right)),
524        BinaryOp::Add => Some(ConstValue::Float(left + right)),
525        BinaryOp::Subtract => Some(ConstValue::Float(left - right)),
526        BinaryOp::GreaterEqual => Some(ConstValue::Int(i32::from(left >= right))),
527        BinaryOp::GreaterThan => Some(ConstValue::Int(i32::from(left > right))),
528        BinaryOp::LessThan => Some(ConstValue::Int(i32::from(left < right))),
529        BinaryOp::LessEqual => Some(ConstValue::Int(i32::from(left <= right))),
530        BinaryOp::NotEqual => Some(ConstValue::Int(i32::from(left != right))),
531        BinaryOp::EqualEqual => Some(ConstValue::Int(i32::from(left == right))),
532        _ => None,
533    }
534}
535
536fn evaluate_string_binary(op: BinaryOp, left: &str, right: &str) -> Option<ConstValue> {
537    match op {
538        BinaryOp::Add => Some(ConstValue::String(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}{1}", left, right))
    })format!("{left}{right}"))),
539        BinaryOp::EqualEqual => Some(ConstValue::Int(i32::from(left == right))),
540        BinaryOp::NotEqual => Some(ConstValue::Int(i32::from(left != right))),
541        _ => None,
542    }
543}
544
545fn assignment_stack_offset(instruction: &NcsInstruction) -> Option<i32> {
546    let bytes = instruction.extra.get(0..4)?;
547    let mut offset = [0u8; 4];
548    offset.copy_from_slice(bytes);
549    Some(i32::from_be_bytes(offset))
550}
551
552#[cfg(test)]
553mod tests {
554    use super::{OptimizationLevel, meld_instructions, optimize_hir};
555    use crate::{
556        BuiltinConstant, BuiltinFunction, BuiltinParameter, BuiltinType, BuiltinValue, LangSpec,
557        NcsAuxCode, NcsInstruction, NcsOpcode, SourceId, compile_hir_to_ncs,
558        decode_ncs_instructions, lower_to_hir, parse_text,
559    };
560
561    fn test_langspec() -> LangSpec {
562        LangSpec {
563            engine_num_structures: 0,
564            engine_structures:     vec![],
565            constants:             vec![
566                BuiltinConstant {
567                    name:  "TRUE".to_string(),
568                    ty:    BuiltinType::Int,
569                    value: BuiltinValue::Int(1),
570                },
571                BuiltinConstant {
572                    name:  "FALSE".to_string(),
573                    ty:    BuiltinType::Int,
574                    value: BuiltinValue::Int(0),
575                },
576            ],
577            functions:             vec![BuiltinFunction {
578                name:        "GetValue".to_string(),
579                return_type: BuiltinType::Int,
580                parameters:  vec![BuiltinParameter {
581                    name:    "nValue".to_string(),
582                    ty:      BuiltinType::Int,
583                    default: None,
584                }],
585            }],
586        }
587    }
588
589    #[test]
590    fn o1_eliminates_dead_functions_in_loader_discovery_order() {
591        let script = parse_text(
592            SourceId::new(1),
593            r#"
594                int NeverUsed() { return 9; }
595                int Leaf() { return 7; }
596                int Mid() { return Leaf(); }
597                int StartingConditional() { return Mid(); }
598            "#,
599            Some(&test_langspec()),
600        )
601        .expect("script should parse");
602        let semantic =
603            crate::analyze_script(&script, Some(&test_langspec())).expect("script should analyze");
604        let hir = lower_to_hir(&script, &semantic, Some(&test_langspec()))
605            .expect("HIR lowering should succeed");
606
607        let optimized = optimize_hir(&hir, Some(&test_langspec()), OptimizationLevel::O1);
608        let names = optimized
609            .functions
610            .iter()
611            .map(|function| function.name.as_str())
612            .collect::<Vec<_>>();
613
614        assert_eq!(names, vec!["StartingConditional", "Mid", "Leaf"]);
615    }
616
617    #[test]
618    fn o2_dead_branch_trimming_removes_calls_from_constant_false_if_branches() {
619        let script = parse_text(
620            SourceId::new(2),
621            r#"
622                int Dead() { return 0; }
623                int Live() { return 1; }
624                int StartingConditional() {
625                    if (FALSE) {
626                        return Dead();
627                    } else {
628                        return Live();
629                    }
630                }
631            "#,
632            Some(&test_langspec()),
633        )
634        .expect("script should parse");
635        let semantic =
636            crate::analyze_script(&script, Some(&test_langspec())).expect("script should analyze");
637        let hir = lower_to_hir(&script, &semantic, Some(&test_langspec()))
638            .expect("HIR lowering should succeed");
639
640        let optimized = optimize_hir(&hir, Some(&test_langspec()), OptimizationLevel::O2);
641        let names = optimized
642            .functions
643            .iter()
644            .map(|function| function.name.as_str())
645            .collect::<Vec<_>>();
646
647        assert_eq!(names, vec!["StartingConditional", "Live"]);
648    }
649
650    #[test]
651    fn o3_melds_local_constant_initializer_pattern() {
652        let script = parse_text(
653            SourceId::new(3),
654            r#"
655                void main() {
656                    int nValue = 3;
657                }
658            "#,
659            Some(&test_langspec()),
660        )
661        .expect("script should parse");
662        let semantic =
663            crate::analyze_script(&script, Some(&test_langspec())).expect("script should analyze");
664        let hir = lower_to_hir(&script, &semantic, Some(&test_langspec()))
665            .expect("HIR lowering should succeed");
666
667        let o0 = decode_ncs_instructions(
668            &compile_hir_to_ncs(&hir, Some(&test_langspec()), OptimizationLevel::O0)
669                .expect("O0 compile should succeed"),
670        )
671        .expect("O0 output should decode");
672        let o3 = decode_ncs_instructions(
673            &compile_hir_to_ncs(&hir, Some(&test_langspec()), OptimizationLevel::O3)
674                .expect("O3 compile should succeed"),
675        )
676        .expect("O3 output should decode");
677
678        assert!(
679            o0.iter()
680                .any(|instruction| instruction.opcode == NcsOpcode::RunstackAdd),
681            "O0 should preserve the local initializer stack dance",
682        );
683        assert!(
684            !o3.iter().any(|instruction| {
685                instruction.opcode == NcsOpcode::Assignment
686                    && instruction.auxcode == NcsAuxCode::TypeVoid
687            }),
688            "O3 should meld the local constant initializer pattern",
689        );
690    }
691
692    #[test]
693    fn meld_instructions_only_rewrites_the_upstream_active_pattern() {
694        let instructions = vec![
695            NcsInstruction {
696                opcode:  NcsOpcode::RunstackAdd,
697                auxcode: NcsAuxCode::TypeInteger,
698                extra:   Vec::new(),
699            },
700            NcsInstruction {
701                opcode:  NcsOpcode::Constant,
702                auxcode: NcsAuxCode::TypeInteger,
703                extra:   3_i32.to_be_bytes().to_vec(),
704            },
705            NcsInstruction {
706                opcode:  NcsOpcode::Assignment,
707                auxcode: NcsAuxCode::TypeVoid,
708                extra:   {
709                    let mut bytes = Vec::new();
710                    bytes.extend_from_slice(&(-8_i32).to_be_bytes());
711                    bytes.extend_from_slice(&(4_u16).to_be_bytes());
712                    bytes
713                },
714            },
715            NcsInstruction {
716                opcode:  NcsOpcode::ModifyStackPointer,
717                auxcode: NcsAuxCode::None,
718                extra:   (-4_i32).to_be_bytes().to_vec(),
719            },
720        ];
721
722        let optimized = meld_instructions(instructions);
723        assert_eq!(optimized.len(), 1);
724        assert_eq!(
725            optimized.first().map(|instruction| instruction.opcode),
726            Some(NcsOpcode::Constant)
727        );
728    }
729}