/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Implementation of `def`.

use std::cell::UnsafeCell;
use std::collections::HashMap;
use std::fmt;
use std::fmt::Display;
use std::fmt::Write;
use std::ptr;
use std::time::Instant;

use allocative::Allocative;
use derivative::Derivative;
use derive_more::Display;
use dupe::Dupe;
use once_cell::sync::Lazy;
use starlark_derive::starlark_value;
use starlark_derive::NoSerialize;
use starlark_derive::VisitSpanMut;

use crate as starlark;
use crate::any::ProvidesStaticType;
use crate::codemap::CodeMap;
use crate::collections::Hashed;
use crate::const_frozen_string;
use crate::docs::DocFunction;
use crate::docs::DocItem;
use crate::docs::DocString;
use crate::docs::DocStringKind;
use crate::docs::DocType;
use crate::environment::FrozenModuleData;
use crate::environment::Globals;
use crate::eval::bc::bytecode::Bc;
use crate::eval::bc::frame::alloca_frame;
use crate::eval::compiler::def_inline::inline_def_body;
use crate::eval::compiler::def_inline::InlineDefBody;
use crate::eval::compiler::expr::ExprCompiled;
use crate::eval::compiler::opt_ctx::OptCtx;
use crate::eval::compiler::scope::payload::CstAssignIdent;
use crate::eval::compiler::scope::payload::CstParameter;
use crate::eval::compiler::scope::payload::CstStmt;
use crate::eval::compiler::scope::payload::CstTypeExpr;
use crate::eval::compiler::scope::Captured;
use crate::eval::compiler::scope::ScopeId;
use crate::eval::compiler::span::IrSpanned;
use crate::eval::compiler::stmt::OptimizeOnFreezeContext;
use crate::eval::compiler::stmt::StmtCompileContext;
use crate::eval::compiler::stmt::StmtsCompiled;
use crate::eval::compiler::Compiler;
use crate::eval::compiler::EvalException;
use crate::eval::runtime::arguments::ArgumentsImpl;
use crate::eval::runtime::arguments::ResolvedArgName;
use crate::eval::runtime::evaluator::Evaluator;
use crate::eval::runtime::frame_span::FrameSpan;
use crate::eval::runtime::frozen_file_span::FrozenFileSpan;
use crate::eval::runtime::params::ParametersSpec;
use crate::eval::runtime::slots::LocalSlotId;
use crate::eval::runtime::slots::LocalSlotIdCapturedOrNot;
use crate::eval::Arguments;
use crate::slice_vec_ext::SliceExt;
use crate::starlark_complex_values;
use crate::syntax::ast::ParameterP;
use crate::values::frozen_ref::AtomicFrozenRefOption;
use crate::values::function::FUNCTION_TYPE;
use crate::values::typing::TypeCompiled;
use crate::values::Freeze;
use crate::values::Freezer;
use crate::values::FrozenHeap;
use crate::values::FrozenRef;
use crate::values::FrozenStringValue;
use crate::values::FrozenValue;
use crate::values::Heap;
use crate::values::StarlarkValue;
use crate::values::Trace;
use crate::values::Tracer;
use crate::values::Value;
use crate::values::ValueLike;

#[derive(thiserror::Error, Debug)]
enum DefError {
    #[error("Function has no type, while function was compiled with return type (internal error)")]
    CheckReturnTypeNoType,
}

/// Store frozen `StmtCompiled`.
/// This is initialized in `post_freeze`.
struct StmtCompiledCell {
    cell: UnsafeCell<Bc>,
}

unsafe impl<'v> Trace<'v> for StmtCompiledCell {
    fn trace(&mut self, _: &Tracer<'v>) {
        // Bytecode contains only frozen values.
    }
}

unsafe impl Sync for StmtCompiledCell {}
unsafe impl Send for StmtCompiledCell {}

impl StmtCompiledCell {
    fn new() -> StmtCompiledCell {
        StmtCompiledCell {
            cell: UnsafeCell::new(Bc::default()),
        }
    }

    /// This function is unsafe if other thread is executing the stmt.
    unsafe fn set(&self, value: Bc) {
        ptr::drop_in_place(self.cell.get());
        ptr::write(self.cell.get(), value);
    }

    fn get(&self) -> &Bc {
        unsafe { &*self.cell.get() }
    }
}

#[derive(Clone, Debug, VisitSpanMut)]
pub(crate) struct ParameterName {
    pub(crate) name: String,
    captured: Captured,
}

#[derive(Clone, Debug, VisitSpanMut)]
pub(crate) enum ParameterCompiled<T> {
    Normal(ParameterName, Option<TypeCompiled<FrozenValue>>),
    WithDefaultValue(ParameterName, Option<TypeCompiled<FrozenValue>>, T),
    Args(ParameterName, Option<TypeCompiled<FrozenValue>>),
    KwArgs(ParameterName, Option<TypeCompiled<FrozenValue>>),
}

impl<T> ParameterCompiled<T> {
    pub(crate) fn map_expr<U>(&self, mut f: impl FnMut(&T) -> U) -> ParameterCompiled<U> {
        match self {
            ParameterCompiled::Normal(n, o) => ParameterCompiled::Normal(n.clone(), *o),
            ParameterCompiled::WithDefaultValue(n, o, t) => {
                ParameterCompiled::WithDefaultValue(n.clone(), *o, f(t))
            }
            ParameterCompiled::Args(n, o) => ParameterCompiled::Args(n.clone(), *o),
            ParameterCompiled::KwArgs(n, o) => ParameterCompiled::KwArgs(n.clone(), *o),
        }
    }

    pub(crate) fn accepts_positional(&self) -> bool {
        match self {
            ParameterCompiled::Normal(_, _) => true,
            ParameterCompiled::WithDefaultValue(_, _, _) => true,
            _ => false,
        }
    }

    pub(crate) fn captured(&self) -> Captured {
        self.name_ty().0.captured
    }

    pub(crate) fn name_ty(&self) -> (&ParameterName, Option<TypeCompiled<FrozenValue>>) {
        match self {
            Self::Normal(n, t) => (n, *t),
            Self::WithDefaultValue(n, t, _) => (n, *t),
            Self::Args(n, t) => (n, *t),
            Self::KwArgs(n, t) => (n, *t),
        }
    }

    pub(crate) fn has_type(&self) -> bool {
        match self.name_ty() {
            (_, Some(_)) => true,
            _ => false,
        }
    }

    pub(crate) fn is_star_or_star_star(&self) -> bool {
        matches!(
            self,
            ParameterCompiled::Args(_, _) | ParameterCompiled::KwArgs(_, _)
        )
    }
}

#[derive(Debug, Clone, VisitSpanMut)]
pub(crate) struct ParametersCompiled<T> {
    pub(crate) params: Vec<IrSpanned<ParameterCompiled<T>>>,
    /// Number of parameters which can be filled positionally.
    /// That is, number of parameters before first `*`, `*args` or `**kwargs`.
    pub(crate) num_positional: u32,
}

impl<T> ParametersCompiled<T> {
    /// How many expressions this parameters references (default values and types).
    pub(crate) fn count_exprs(&self) -> u32 {
        let mut count = 0;
        for p in &self.params {
            p.map_expr(|_e| count += 1);
        }
        count
    }

    /// How many parameter variables?
    pub(crate) fn count_param_variables(&self) -> u32 {
        self.params.len().try_into().unwrap()
    }

    /// Any parameter has type annotation?
    pub(crate) fn has_types(&self) -> bool {
        self.params.iter().any(|p| p.has_type())
    }

    /// Has `*args` or `*kwargs` parameter? `*` is fine.
    pub(crate) fn has_args_or_kwargs(&self) -> bool {
        self.params.iter().any(|p| {
            matches!(
                p.node,
                ParameterCompiled::Args(..) | ParameterCompiled::KwArgs(..)
            )
        })
    }

    pub(crate) fn parameter_captures(&self) -> Vec<LocalSlotId> {
        self.params
            .iter()
            .enumerate()
            .filter_map(|(i, p)| {
                if p.captured() == Captured::Yes {
                    Some(LocalSlotId(i as u32))
                } else {
                    None
                }
            })
            .collect()
    }
}

/// Copy local variable slot to nested function.
#[derive(Debug, Clone, Dupe)]
pub(crate) struct CopySlotFromParent {
    /// Slot in the outer function.
    pub(crate) parent: LocalSlotIdCapturedOrNot,
    /// Slot in the nested function.
    pub(crate) child: LocalSlotIdCapturedOrNot,
}

/// Static info for `def`, `lambda` or module.
#[derive(Derivative, Display)]
#[derivative(Debug)]
#[display(fmt = "DefInfo")]
pub(crate) struct DefInfo {
    pub(crate) name: FrozenStringValue,
    /// Span of function signature.
    pub(crate) signature_span: FrozenFileSpan,
    /// Indices of parameters, which are captured in nested defs.
    parameter_captures: FrozenRef<'static, [LocalSlotId]>,
    /// Codemap of the file where the function is declared.
    pub(crate) codemap: FrozenRef<'static, CodeMap>,
    /// The raw docstring pulled out of the AST.
    pub(crate) docstring: Option<String>,
    /// Slots this scope uses, including for parameters and `parent`.
    /// Indexed by [`LocalSlotId`], values are variable names.
    pub(crate) used: FrozenRef<'static, [FrozenStringValue]>,
    /// Slots to copy from the parent.
    /// Module-level identifiers are not copied over, to avoid excess copying.
    pub(crate) parent: FrozenRef<'static, [CopySlotFromParent]>,
    /// Statement compiled for non-frozen def.
    #[derivative(Debug = "ignore")]
    stmt_compiled: Bc,
    // The compiled expression for the body of this definition, to be run
    // after the parameters are evaluated.
    #[derivative(Debug = "ignore")]
    body_stmts: StmtsCompiled,
    /// How to compile the statement on freeze.
    stmt_compile_context: StmtCompileContext,
    /// Function can be inlined.
    pub(crate) inline_def_body: Option<InlineDefBody>,
    /// Globals captured during function or module creation.
    /// Only needed for debugger evaluation.
    pub(crate) globals: FrozenRef<'static, Globals>,
}

impl DefInfo {
    pub(crate) fn empty() -> FrozenRef<'static, DefInfo> {
        static EMPTY: Lazy<DefInfo> = Lazy::new(|| DefInfo {
            name: const_frozen_string!("<empty>"),
            signature_span: FrozenFileSpan::default(),
            parameter_captures: FrozenRef::new(&[]),
            codemap: FrozenRef::new(CodeMap::empty_static()),
            docstring: None,
            used: FrozenRef::new(&[]),
            parent: FrozenRef::new(&[]),
            stmt_compiled: Bc::default(),
            body_stmts: StmtsCompiled::empty(),
            stmt_compile_context: StmtCompileContext::default(),
            inline_def_body: None,
            globals: FrozenRef::new(Globals::empty()),
        });
        FrozenRef::new(&EMPTY)
    }

    pub(crate) fn for_module(
        codemap: FrozenRef<'static, CodeMap>,
        local_names: FrozenRef<'static, [FrozenStringValue]>,
        parent: FrozenRef<'static, [CopySlotFromParent]>,
        globals: FrozenRef<'static, Globals>,
    ) -> DefInfo {
        DefInfo {
            name: const_frozen_string!("<module>"),
            signature_span: FrozenFileSpan::default(),
            parameter_captures: FrozenRef::new(&[]),
            codemap,
            docstring: None,
            used: local_names,
            parent,
            stmt_compiled: Bc::default(),
            body_stmts: StmtsCompiled::empty(),
            stmt_compile_context: StmtCompileContext::default(),
            inline_def_body: None,
            globals,
        }
    }
}

#[derive(Clone, Debug, VisitSpanMut)]
pub(crate) struct DefCompiled {
    pub(crate) function_name: String,
    pub(crate) params: ParametersCompiled<IrSpanned<ExprCompiled>>,
    pub(crate) return_type: Option<TypeCompiled<FrozenValue>>,
    pub(crate) info: FrozenRef<'static, DefInfo>,
}

impl Compiler<'_, '_, '_> {
    fn parameter_name(&mut self, ident: &CstAssignIdent) -> ParameterName {
        let binding_id = ident.1.expect("no binding for parameter");
        let binding = self.scope_data.get_binding(binding_id);
        ParameterName {
            name: ident.node.0.clone(),
            captured: binding.captured,
        }
    }

    /// Compile a parameter. Return `None` for `*` pseudo parameter.
    fn parameter(
        &mut self,
        x: &CstParameter,
    ) -> Option<IrSpanned<ParameterCompiled<IrSpanned<ExprCompiled>>>> {
        let span = FrameSpan::new(FrozenFileSpan::new(self.codemap, x.span));
        Some(IrSpanned {
            span,
            node: match &x.node {
                ParameterP::Normal(x, t) => ParameterCompiled::Normal(
                    self.parameter_name(x),
                    self.expr_for_type(t.as_deref()).map(|t| t.node),
                ),
                ParameterP::WithDefaultValue(x, t, v) => ParameterCompiled::WithDefaultValue(
                    self.parameter_name(x),
                    self.expr_for_type(t.as_deref()).map(|t| t.node),
                    self.expr(v),
                ),
                ParameterP::NoArgs => return None,
                ParameterP::Args(x, t) => ParameterCompiled::Args(
                    self.parameter_name(x),
                    self.expr_for_type(t.as_deref()).map(|t| t.node),
                ),
                ParameterP::KwArgs(x, t) => ParameterCompiled::KwArgs(
                    self.parameter_name(x),
                    self.expr_for_type(t.as_deref()).map(|t| t.node),
                ),
            },
        })
    }

    pub fn function(
        &mut self,
        name: &str,
        signature_span: FrozenFileSpan,
        scope_id: ScopeId,
        params: &[CstParameter],
        return_type: Option<&CstTypeExpr>,
        suite: &CstStmt,
    ) -> ExprCompiled {
        let file = self.codemap.file_span(suite.span);
        let function_name = format!("{}.{}", file.file.filename(), name);
        let name = self.eval.frozen_heap().alloc_str(name);

        // The parameters run in the scope of the parent, so compile them with the outer
        // scope
        let num_positional = params
            .iter()
            .position(|x| {
                matches!(
                    &x.node,
                    ParameterP::NoArgs | ParameterP::Args(..) | ParameterP::KwArgs(..)
                )
            })
            .unwrap_or(params.len())
            .try_into()
            .unwrap();
        let params = params.iter().filter_map(|x| self.parameter(x)).collect();
        let params = ParametersCompiled {
            params,
            num_positional,
        };
        let return_type = self.expr_for_type(return_type).map(|t| t.node);

        self.enter_scope(scope_id);

        let docstring = DocString::extract_raw_starlark_docstring(&suite);
        let body = self.stmt(suite, false);
        let scope_id = self.exit_scope();
        let scope_names = self.scope_data.get_scope(scope_id);

        let has_types = return_type.is_some() || params.has_types();

        let inline_def_body = if has_types {
            // It is harder to inline if a function declares parameter types or return type.
            None
        } else {
            inline_def_body(&params, &body)
        };

        let param_count = params.count_param_variables();

        let used = self
            .eval
            .frozen_heap()
            .alloc_any_slice_display_from_debug(&scope_names.used);
        let info = self.eval.module_env.frozen_heap().alloc_any(DefInfo {
            name,
            signature_span,
            parameter_captures: self
                .eval
                .frozen_heap()
                .alloc_any_slice_display_from_debug(&params.parameter_captures()),
            codemap: self.codemap,
            docstring,
            used,
            parent: self
                .eval
                .frozen_heap()
                .alloc_any_slice_display_from_debug(&scope_names.parent),
            stmt_compiled: body.as_bc(
                &self.compile_context(return_type.is_some()),
                used,
                param_count,
                self.eval.module_env.frozen_heap(),
            ),
            body_stmts: body,
            inline_def_body,
            stmt_compile_context: self.compile_context(return_type.is_some()),
            globals: self.globals,
        });

        ExprCompiled::Def(DefCompiled {
            function_name,
            params,
            return_type,
            info,
        })
    }
}

/// Starlark function internal representation and implementation of
/// [`StarlarkValue`].
#[derive(Derivative, NoSerialize, ProvidesStaticType, Trace, Allocative)]
#[derivative(Debug)]
pub(crate) struct DefGen<V> {
    pub(crate) parameters: ParametersSpec<V>, // The parameters, **kwargs etc including defaults (which are evaluated afresh each time)
    /// Indices of parameters, which are captured in nested defs.
    /// This is a copy of `DefInfo.parameter_captures`.
    parameter_captures: FrozenRef<'static, [LocalSlotId]>,
    // The types of the parameters.
    // (Sparse indexed array, (0, argm T) implies parameter 0 named arg must have type T).
    parameter_types: Vec<(LocalSlotId, String, TypeCompiled<FrozenValue>)>,
    pub(crate) return_type: Option<TypeCompiled<FrozenValue>>, // The return type annotation for the function
    /// Data created during function compilation but before function instantiation.
    /// `DefInfo` can be shared by multiple `def` instances, for example,
    /// `lambda` functions can be instantiated multiple times.
    pub(crate) def_info: FrozenRef<'static, DefInfo>,
    /// Any variables captured from the outer scope (nested def/lambda).
    /// Values are either [`Value`] or [`FrozenValue`] pointing respectively to
    /// [`ValueCaptured`] or [`FrozenValueCaptured`].
    captured: Vec<V>,
    // Important to ignore these field as it probably references DefGen in a cycle
    #[derivative(Debug = "ignore")]
    /// A reference to the module where the function is defined after the module has been frozen.
    /// When the module is not frozen yet, this field contains `None`, and function's module
    /// can be accessed from evaluator's module.
    #[allocative(skip)]
    module: AtomicFrozenRefOption<FrozenModuleData>,
    /// This field is only used in `FrozenDef`. It is populated in `post_freeze`.
    #[derivative(Debug = "ignore")]
    #[allocative(skip)]
    optimized_on_freeze_stmt: StmtCompiledCell,
}

impl<V> Display for DefGen<V> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.parameters.signature())
    }
}

pub(crate) type Def<'v> = DefGen<Value<'v>>;
pub(crate) type FrozenDef = DefGen<FrozenValue>;

starlark_complex_values!(Def);

impl<'v> Def<'v> {
    pub(crate) fn new(
        parameters: ParametersSpec<Value<'v>>,
        parameter_types: Vec<(LocalSlotId, String, TypeCompiled<FrozenValue>)>,
        return_type: Option<TypeCompiled<FrozenValue>>,
        stmt: FrozenRef<'static, DefInfo>,
        eval: &mut Evaluator<'v, '_>,
    ) -> Value<'v> {
        let captured = stmt
            .parent
            .as_ref()
            .map(|copy| eval.clone_slot_capture(copy, &stmt));
        eval.heap().alloc(Self {
            parameters,
            parameter_captures: stmt.parameter_captures,
            parameter_types,
            return_type,
            captured,
            module: AtomicFrozenRefOption::new(eval.module_variables),
            optimized_on_freeze_stmt: StmtCompiledCell::new(),
            def_info: stmt,
        })
    }
}

impl<'v, T1: ValueLike<'v>> DefGen<T1> {
    fn docs(&self) -> Option<DocItem> {
        let parameter_types: HashMap<usize, DocType> = self
            .parameter_types
            .iter()
            .map(|(idx, _, ty)| {
                (
                    idx.0 as usize,
                    DocType {
                        raw_type: ty.as_ty(),
                    },
                )
            })
            .collect();

        let return_type = self.return_type.map(|r| DocType {
            raw_type: r.as_ty(),
        });

        let function_docs = DocFunction::from_docstring(
            DocStringKind::Starlark,
            self.parameters
                .documentation(parameter_types, HashMap::new()),
            return_type,
            self.def_info.docstring.as_ref().map(String::as_ref),
            None,
        );

        Some(DocItem::Function(function_docs))
    }
}

impl<'v> Freeze for Def<'v> {
    type Frozen = FrozenDef;

    fn freeze(self, freezer: &Freezer) -> anyhow::Result<Self::Frozen> {
        let parameters = self.parameters.freeze(freezer)?;
        let parameter_types = self.parameter_types.freeze(freezer)?;
        let return_type = self.return_type.freeze(freezer)?;
        let captured = self.captured.try_map(|x| x.freeze(freezer))?;
        let module = AtomicFrozenRefOption::new(self.module.load_relaxed());
        Ok(FrozenDef {
            parameters,
            parameter_captures: self.parameter_captures,
            parameter_types,
            return_type,
            def_info: self.def_info,
            captured,
            module,
            optimized_on_freeze_stmt: self.optimized_on_freeze_stmt,
        })
    }
}

pub(crate) trait DefLike<'v> {
    const FROZEN: bool;
}

impl<'v> DefLike<'v> for DefGen<Value<'v>> {
    const FROZEN: bool = false;
}

impl<'v> DefLike<'v> for DefGen<FrozenValue> {
    const FROZEN: bool = true;
}

#[starlark_value(type = FUNCTION_TYPE)]
impl<'v, V: ValueLike<'v> + 'v> StarlarkValue<'v> for DefGen<V>
where
    Self: ProvidesStaticType<'v> + DefLike<'v>,
{
    fn name_for_call_stack(&self, _me: Value<'v>) -> String {
        self.def_info.name.as_str().to_owned()
    }

    fn invoke(
        &self,
        me: Value<'v>,
        args: &Arguments<'v, '_>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>> {
        self.invoke_impl(me, &args.0, eval)
    }

    fn documentation(&self) -> Option<DocItem> {
        self.docs()
    }
}

impl<'v, V: ValueLike<'v>> DefGen<V>
where
    Self: DefLike<'v>,
{
    pub(crate) fn bc(&self) -> &Bc {
        if Self::FROZEN {
            self.optimized_on_freeze_stmt.get()
        } else {
            &self.def_info.stmt_compiled
        }
    }

    fn check_parameter_types(&self, eval: &mut Evaluator<'v, '_>) -> anyhow::Result<()> {
        let start = if eval.typecheck_profile.enabled {
            Some(Instant::now())
        } else {
            None
        };
        for (i, arg_name, ty) in &self.parameter_types {
            match eval.current_frame.get_slot(i.to_captured_or_not()) {
                None => {
                    panic!("Not allowed optional unassigned with type annotations on them")
                }
                Some(v) => ty.check_type(v, Some(arg_name))?,
            }
        }
        if let Some(start) = start {
            eval.typecheck_profile
                .add(self.def_info.name, start.elapsed());
        }
        Ok(())
    }

    pub(crate) fn check_return_type(
        &self,
        ret: Value<'v>,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<()> {
        let return_type_ty: TypeCompiled<FrozenValue> =
            self.return_type.ok_or(DefError::CheckReturnTypeNoType)?;
        let start = if eval.typecheck_profile.enabled {
            Some(Instant::now())
        } else {
            None
        };
        return_type_ty.check_type(ret, None)?;
        if let Some(start) = start {
            eval.typecheck_profile
                .add(self.def_info.name, start.elapsed());
        }
        Ok(())
    }

    #[inline(always)]
    fn invoke_impl<'a, A: ArgumentsImpl<'v, 'a>>(
        &self,
        me: Value<'v>,
        args: &A,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>>
    where
        'v: 'a,
    {
        let bc = self.bc();
        alloca_frame(
            eval,
            bc.local_count,
            bc.max_stack_size,
            bc.max_loop_depth,
            |eval| {
                let slots = eval.current_frame.locals();
                self.parameters.collect_inline(args, slots, eval.heap())?;
                self.invoke_raw(me, eval)
            },
        )
    }

    pub(crate) fn invoke_with_args<'a, A: ArgumentsImpl<'v, 'a>>(
        &self,
        me: Value<'v>,
        args: &A,
        eval: &mut Evaluator<'v, '_>,
    ) -> anyhow::Result<Value<'v>>
    where
        'v: 'a,
    {
        // This is trivial function which delegates to `invoke_impl`.
        // `invoke_impl` is called from two places,
        // giving this function different name makes this function easier to see in profiler.
        self.invoke_impl(me, args, eval)
    }

    /// Invoke the function, assuming that:
    /// * the frame has been allocated and stored in `eval.current_frame`
    /// * the arguments have been collected into the frame
    #[inline(always)]
    fn invoke_raw(&self, me: Value<'v>, eval: &mut Evaluator<'v, '_>) -> anyhow::Result<Value<'v>> {
        // println!("invoking {}", self.def.stmt.name.node);

        if !self.parameter_types.is_empty() {
            self.check_parameter_types(eval)?;
        }

        // Parameters are collected into local slots without captures
        // (to avoid even more branches in parameter capture),
        // and this loop wraps captured parameters.
        for &captured in &*self.parameter_captures {
            eval.wrap_local_slot_captured(captured);
        }

        // Copy over the parent slots.
        // Explicitly check `self.captured` is not empty to avoid accessing
        // self.def_info.scope_names which is two indirections.
        if !self.captured.is_empty() {
            for (copy, captured) in self.def_info.parent.iter().zip(self.captured.iter()) {
                eval.current_frame.set_slot(copy.child, captured.to_value());
            }
        }

        if Self::FROZEN {
            debug_assert!(self.module.load_relaxed().is_some());
        }
        let res = eval.with_function_context(me, self.module.load_relaxed(), self.bc());

        res.map_err(|EvalException(e)| e)
    }

    pub(crate) fn resolve_arg_name(&self, name: Hashed<&str>) -> ResolvedArgName {
        self.parameters.resolve_name(name)
    }

    pub(crate) fn dump_debug(&self) -> String {
        let mut w = String::new();
        writeln!(w, "Bytecode:").unwrap();
        self.bc()
            .dump_debug()
            .lines()
            .for_each(|l| writeln!(w, "  {}", l).unwrap());
        w
    }
}

impl FrozenDef {
    pub(crate) fn post_freeze(
        &self,
        module: FrozenRef<FrozenModuleData>,
        heap: &Heap,
        frozen_heap: &FrozenHeap,
    ) {
        // Module passed to this function is not always module where the function is declared:
        // A function can be created in a frozen module and frozen later in another module.
        // `def_module` variable contains a module where this `def` is declared.
        let def_module = match self.module.load_relaxed() {
            None => {
                self.module.store_relaxed(module);
                module
            }
            Some(module) => module,
        };

        // Now perform the optimization of function body with fully frozen module:
        // all module variables are frozen, so we can inline more aggressively.
        let body_optimized = self
            .def_info
            .body_stmts
            .optimize(&mut OptCtx::new(
                &mut OptimizeOnFreezeContext {
                    module: def_module.as_ref(),
                    heap,
                    frozen_heap,
                },
                self.parameters.len().try_into().unwrap(),
            ))
            .as_bc(
                &self.def_info.stmt_compile_context,
                self.def_info.used,
                self.parameters.len() as u32,
                frozen_heap,
            );

        // Store the optimized body.
        // This is (relatively) safe because we know that during freeze
        // nobody has a reference to stmt: nobody is executing this `def`.
        unsafe {
            self.optimized_on_freeze_stmt.set(body_optimized);
        }
    }
}


/*
 * Copyright 2018 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Evaluation of an expression.

use std::borrow::Cow;
use std::cmp::Ordering;

use dupe::Dupe;
use starlark_derive::VisitSpanMut;
use thiserror::Error;

use crate::codemap::Spanned;
use crate::collections::symbol_map::Symbol;
use crate::environment::slots::ModuleSlotId;
use crate::errors::did_you_mean::did_you_mean;
use crate::eval::compiler::args::ArgsCompiledValue;
use crate::eval::compiler::call::CallCompiled;
use crate::eval::compiler::compr::ComprCompiled;
use crate::eval::compiler::constants::Constants;
use crate::eval::compiler::def::DefCompiled;
use crate::eval::compiler::def::FrozenDef;
use crate::eval::compiler::expr_bool::ExprCompiledBool;
use crate::eval::compiler::known::list_to_tuple;
use crate::eval::compiler::opt_ctx::OptCtx;
use crate::eval::compiler::scope::payload::CstExpr;
use crate::eval::compiler::scope::payload::CstIdent;
use crate::eval::compiler::scope::AssignCount;
use crate::eval::compiler::scope::Captured;
use crate::eval::compiler::scope::ResolvedIdent;
use crate::eval::compiler::scope::Slot;
use crate::eval::compiler::span::IrSpanned;
use crate::eval::compiler::Compiler;
use crate::eval::runtime::frame_span::FrameSpan;
use crate::eval::runtime::frozen_file_span::FrozenFileSpan;
use crate::eval::runtime::slots::LocalCapturedSlotId;
use crate::eval::runtime::slots::LocalSlotId;
use crate::slice_vec_ext::SliceExt;
use crate::syntax::ast::AstExprP;
use crate::syntax::ast::AstLiteral;
use crate::syntax::ast::AstPayload;
use crate::syntax::ast::BinOp;
use crate::syntax::ast::ExprP;
use crate::syntax::ast::LambdaP;
use crate::syntax::ast::StmtP;
use crate::values::function::BoundMethodGen;
use crate::values::function::FrozenBoundMethod;
use crate::values::layout::value_not_special::FrozenValueNotSpecial;
use crate::values::list::ListRef;
use crate::values::string::interpolation::parse_percent_s_one;
use crate::values::types::bool::StarlarkBool;
use crate::values::types::dict::Dict;
use crate::values::types::float::StarlarkFloat;
use crate::values::types::inline_int::InlineInt;
use crate::values::types::list::value::FrozenListData;
use crate::values::types::list::value::ListData;
use crate::values::types::range::Range;
use crate::values::types::string::dot_format::format_one;
use crate::values::types::string::interpolation::percent_s_one;
use crate::values::types::tuple::value::Tuple;
use crate::values::types::unbound::MaybeUnboundValue;
use crate::values::FrozenHeap;
use crate::values::FrozenStringValue;
use crate::values::FrozenValue;
use crate::values::FrozenValueTyped;
use crate::values::Heap;
use crate::values::StarlarkValue;
use crate::values::Value;
use crate::values::ValueError;
use crate::values::ValueLike;

/// `bool` operation.
#[derive(Copy, Clone, Dupe, Eq, PartialEq, Debug)]
pub(crate) enum MaybeNot {
    Id,
    Not,
}

impl MaybeNot {
    pub(crate) fn negate(self) -> MaybeNot {
        match self {
            MaybeNot::Id => MaybeNot::Not,
            MaybeNot::Not => MaybeNot::Id,
        }
    }
}

/// Map result of comparison to boolean.
#[derive(Copy, Clone, Dupe, Debug)]
pub(crate) enum CompareOp {
    Less,
    Greater,
    LessOrEqual,
    GreaterOrEqual,
}

impl CompareOp {
    fn apply(self, x: Ordering) -> bool {
        match self {
            CompareOp::Less => x == Ordering::Less,
            CompareOp::Greater => x == Ordering::Greater,
            CompareOp::LessOrEqual => x != Ordering::Greater,
            CompareOp::GreaterOrEqual => x != Ordering::Less,
        }
    }
}

/// Builtin function with one argument.
#[derive(Clone, Debug, VisitSpanMut)]
pub(crate) enum Builtin1 {
    Minus,
    /// `+x`.
    Plus,
    /// `~x`.
    BitNot,
    /// `not x`.
    Not,
    /// `type(arg) == "y"`
    TypeIs(FrozenStringValue),
    /// `"aaa%sbbb" % arg`
    PercentSOne(FrozenStringValue, FrozenStringValue),
    /// `"aaa%sbbb".format(arg)`
    FormatOne(FrozenStringValue, FrozenStringValue),
    /// `x.field`.
    Dot(Symbol),
}

impl Builtin1 {
    fn eval<'v>(&self, v: FrozenValue, ctx: &mut OptCtx<'v, '_, '_>) -> Option<Value<'v>> {
        match self {
            Builtin1::Minus => v.to_value().minus(ctx.heap()).ok(),
            Builtin1::Plus => v.to_value().plus(ctx.heap()).ok(),
            Builtin1::BitNot => v.to_value().bit_not(ctx.heap()).ok(),
            Builtin1::Not => Some(Value::new_bool(!v.to_value().to_bool())),
            Builtin1::TypeIs(t) => Some(Value::new_bool(v.to_value().get_type_value() == *t)),
            Builtin1::FormatOne(before, after) => {
                Some(format_one(before, v.to_value(), after, ctx.heap()).to_value())
            }
            Builtin1::PercentSOne(before, after) => {
                percent_s_one(before, v.to_value(), after, ctx.heap())
                    .map(|s| s.to_value())
                    .ok()
            }
            Builtin1::Dot(field) => {
                Some(ExprCompiled::compile_time_getattr(v, field, ctx)?.to_value())
            }
        }
    }
}

/// Builtin function with two arguments.
#[derive(Copy, Clone, Dupe, Debug, VisitSpanMut)]
pub(crate) enum Builtin2 {
    /// `a == b`.
    Equals,
    /// `a in b`.
    In,
    /// `a - b`.
    Sub,
    /// `a + b`.
    Add,
    /// `a * b`.
    Multiply,
    /// `a % b`.
    Percent,
    /// `a / b`.
    Divide,
    /// `a // b`.
    FloorDivide,
    /// `a & b`.
    BitAnd,
    /// `a | b`.
    BitOr,
    /// `a ^ b`.
    BitXor,
    /// `a << b`.
    LeftShift,
    /// `a >> b`.
    RightShift,
    /// `a <=> b`.
    Compare(CompareOp),
    /// `a[b]`.
    ArrayIndex,
}

impl Builtin2 {
    fn eval<'v>(self, a: Value<'v>, b: Value<'v>, heap: &'v Heap) -> anyhow::Result<Value<'v>> {
        match self {
            Builtin2::Equals => a.equals(b).map(Value::new_bool),
            Builtin2::Compare(cmp) => a.compare(b).map(|c| Value::new_bool(cmp.apply(c))),
            Builtin2::In => b.is_in(a).map(Value::new_bool),
            Builtin2::Sub => a.sub(b, heap),
            Builtin2::Add => a.add(b, heap),
            Builtin2::Multiply => a.mul(b, heap),
            Builtin2::Percent => a.percent(b, heap),
            Builtin2::Divide => a.div(b, heap),
            Builtin2::FloorDivide => a.floor_div(b, heap),
            Builtin2::BitAnd => a.bit_and(b, heap),
            Builtin2::BitOr => a.bit_or(b, heap),
            Builtin2::BitXor => a.bit_xor(b, heap),
            Builtin2::LeftShift => a.left_shift(b, heap),
            Builtin2::RightShift => a.right_shift(b, heap),
            Builtin2::ArrayIndex => a.at(b, heap),
        }
    }
}

/// Logical binary operator.
#[derive(Copy, Clone, Dupe, Debug, VisitSpanMut, Eq, PartialEq)]
pub(crate) enum ExprLogicalBinOp {
    And,
    Or,
}

#[derive(Clone, Debug, VisitSpanMut)]
pub(crate) enum ExprCompiled {
    Value(FrozenValue),
    /// Read local non-captured variable.
    Local(LocalSlotId),
    /// Read local captured variable.
    LocalCaptured(LocalCapturedSlotId),
    Module(ModuleSlotId),
    Tuple(Vec<IrSpanned<ExprCompiled>>),
    List(Vec<IrSpanned<ExprCompiled>>),
    Dict(Vec<(IrSpanned<ExprCompiled>, IrSpanned<ExprCompiled>)>),
    /// Comprehension.
    Compr(ComprCompiled),
    If(
        Box<(
            // Condition.
            IrSpanned<ExprCompiled>,
            // Then branch.
            IrSpanned<ExprCompiled>,
            // Else branch.
            IrSpanned<ExprCompiled>,
        )>,
    ),
    Slice(
        Box<(
            IrSpanned<ExprCompiled>,
            Option<IrSpanned<ExprCompiled>>,
            Option<IrSpanned<ExprCompiled>>,
            Option<IrSpanned<ExprCompiled>>,
        )>,
    ),
    Builtin1(Builtin1, Box<IrSpanned<ExprCompiled>>),
    LogicalBinOp(
        ExprLogicalBinOp,
        Box<(IrSpanned<ExprCompiled>, IrSpanned<ExprCompiled>)>,
    ),
    /// Expression equivalent to `(x, y)[1]`: evaluate `x`, discard the result,
    /// then evaluate `y` and use its result.
    Seq(Box<(IrSpanned<ExprCompiled>, IrSpanned<ExprCompiled>)>),
    Builtin2(
        Builtin2,
        Box<(IrSpanned<ExprCompiled>, IrSpanned<ExprCompiled>)>,
    ),
    Index2(
        Box<(
            IrSpanned<ExprCompiled>,
            IrSpanned<ExprCompiled>,
            IrSpanned<ExprCompiled>,
        )>,
    ),
    Call(Box<IrSpanned<CallCompiled>>),
    Def(DefCompiled),
}

impl ExprCompiled {
    pub fn as_value(&self) -> Option<FrozenValue> {
        match self {
            Self::Value(x) => Some(*x),
            _ => None,
        }
    }

    /// Expression is known to be a constant which is a `def`.
    pub(crate) fn as_frozen_def(&self) -> Option<FrozenValueTyped<FrozenDef>> {
        FrozenValueTyped::new(self.as_value()?)
    }

    /// Expression is known to be a frozen bound method.
    pub(crate) fn as_frozen_bound_method(&self) -> Option<FrozenValueTyped<FrozenBoundMethod>> {
        FrozenValueTyped::new(self.as_value()?)
    }

    /// Expression is builtin `len` function.
    pub(crate) fn is_fn_len(&self) -> bool {
        match self.as_value() {
            Some(value) => value == Constants::get().fn_len,
            None => false,
        }
    }

    /// Expression is builtin `type` function.
    pub(crate) fn is_fn_type(&self) -> bool {
        match self.as_value() {
            Some(value) => value == Constants::get().fn_type,
            None => false,
        }
    }

    /// If expression is `type(x)`, return `x`.
    pub(crate) fn as_type(&self) -> Option<&IrSpanned<ExprCompiled>> {
        match self {
            Self::Call(c) => c.as_type(),
            _ => None,
        }
    }

    /// If expression if `type(x) == t`, return `x` and `t`.
    pub(crate) fn as_type_is(&self) -> Option<(&IrSpanned<ExprCompiled>, FrozenStringValue)> {
        match self {
            ExprCompiled::Builtin1(Builtin1::TypeIs(t), x) => Some((x, *t)),
            _ => None,
        }
    }

    /// Expression is a frozen value which is builtin.
    pub(crate) fn as_builtin_value(&self) -> Option<FrozenValue> {
        match self {
            Self::Value(x) if x.is_builtin() => Some(*x),
            _ => None,
        }
    }

    /// Is expression a constant string?
    pub(crate) fn as_string(&self) -> Option<FrozenStringValue> {
        FrozenStringValue::new(self.as_value()?)
    }

    /// Try to extract `[c0, c1, ..., cn]` from this expression.
    pub(crate) fn as_short_list_of_consts(&self) -> Option<Vec<FrozenValue>> {
        // Prevent exponential explosion during optimization.
        const MAX_LEN: usize = 1000;
        match self {
            ExprCompiled::List(xs) if xs.len() <= MAX_LEN => {
                xs.try_map(|x| x.as_value().ok_or(())).ok()
            }
            ExprCompiled::Value(v) => {
                let list = FrozenListData::from_frozen_value(v)?;
                if list.len() <= MAX_LEN {
                    Some(list.content().to_owned())
                } else {
                    None
                }
            }
            _ => None,
        }
    }

    /// Iterable produced by this expression results in empty.
    pub(crate) fn is_iterable_empty(&self) -> bool {
        match self {
            ExprCompiled::List(xs) => xs.is_empty(),
            ExprCompiled::Tuple(xs) => xs.is_empty(),
            ExprCompiled::Dict(xs) => xs.is_empty(),
            ExprCompiled::Value(v) if v.is_builtin() => {
                v.to_value().length().map_or(false, |l| l == 0)
            }
            _ => false,
        }
    }

    /// Result of this expression is definitely `bool`
    /// (if `false` it may also be `bool`).
    fn is_definitely_bool(&self) -> bool {
        match self {
            Self::Value(v) => v.unpack_bool().is_some(),
            Self::Builtin1(Builtin1::Not | Builtin1::TypeIs(_), _)
            | Self::Builtin2(Builtin2::In | Builtin2::Equals | Builtin2::Compare(_), ..) => true,
            _ => false,
        }
    }

    /// This expression is definitely:
    /// * infallible
    /// * has no effects
    pub(crate) fn is_pure_infallible(&self) -> bool {
        match self {
            Self::Value(..) => true,
            Self::List(xs) | Self::Tuple(xs) => xs.iter().all(|x| x.is_pure_infallible()),
            Self::Dict(xs) => xs.is_empty(),
            Self::Builtin1(Builtin1::Not | Builtin1::TypeIs(_), x) => x.is_pure_infallible(),
            Self::Seq(x_y) => {
                let (x, y) = &**x_y;
                x.is_pure_infallible() && y.is_pure_infallible()
            }
            Self::LogicalBinOp(_op, x_y) => {
                let (x, y) = &**x_y;
                x.is_pure_infallible() && y.is_pure_infallible()
            }
            Self::If(cond_x_y) => {
                let (cond, x, y) = &**cond_x_y;
                cond.is_pure_infallible() && x.is_pure_infallible() && y.is_pure_infallible()
            }
            Self::Call(call) => call.is_pure_infallible(),
            _ => false,
        }
    }

    /// If this expression is pure, infallible, and known to produce a value,
    /// return truth of that value.
    pub(crate) fn is_pure_infallible_to_bool(&self) -> Option<bool> {
        match self {
            ExprCompiled::Value(v) => Some(v.to_value().to_bool()),
            ExprCompiled::List(xs) | ExprCompiled::Tuple(xs)
                if xs.iter().all(|x| x.is_pure_infallible()) =>
            {
                Some(!xs.is_empty())
            }
            // TODO(nga): if keys are unique hashable constants, we can fold this to constant too.
            ExprCompiled::Dict(xs) if xs.is_empty() => Some(false),
            ExprCompiled::Builtin1(Builtin1::Not, x) => x.is_pure_infallible_to_bool().map(|x| !x),
            ExprCompiled::LogicalBinOp(op, x_y) => {
                let (x, y) = &**x_y;
                match (
                    op,
                    x.is_pure_infallible_to_bool(),
                    y.is_pure_infallible_to_bool(),
                ) {
                    (ExprLogicalBinOp::And, Some(true), y) => y,
                    (ExprLogicalBinOp::Or, Some(false), y) => y,
                    (ExprLogicalBinOp::And, Some(false), _) => Some(false),
                    (ExprLogicalBinOp::Or, Some(true), _) => Some(true),
                    (_, None, _) => None,
                }
            }
            _ => None,
        }
    }

    /// This expression is local slot.
    pub(crate) fn as_local_non_captured(&self) -> Option<LocalSlotId> {
        match self {
            ExprCompiled::Local(slot) => Some(*slot),
            _ => None,
        }
    }
}

impl IrSpanned<ExprCompiled> {
    pub(crate) fn optimize(&self, ctx: &mut OptCtx) -> IrSpanned<ExprCompiled> {
        let span = self.span;
        let expr = match &self.node {
            e @ (ExprCompiled::Value(..)
            | ExprCompiled::Local(..)
            | ExprCompiled::LocalCaptured(..)) => e.clone(),
            ExprCompiled::Module(slot) => {
                match ctx.frozen_module().and_then(|m| m.get_slot(*slot)) {
                    None => {
                        // Let if fail at runtime.
                        ExprCompiled::Module(*slot)
                    }
                    Some(v) => ExprCompiled::Value(v),
                }
            }
            ExprCompiled::Tuple(xs) => {
                ExprCompiled::tuple(xs.map(|e| e.optimize(ctx)), ctx.frozen_heap())
            }
            ExprCompiled::List(xs) => ExprCompiled::List(xs.map(|e| e.optimize(ctx))),
            ExprCompiled::Dict(kvs) => {
                ExprCompiled::Dict(kvs.map(|(k, v)| (k.optimize(ctx), v.optimize(ctx))))
            }
            ExprCompiled::Compr(compr) => compr.optimize(ctx),
            ExprCompiled::If(cond_t_f) => {
                let (cond, t, f) = &**cond_t_f;
                let cond = cond.optimize(ctx);
                let t = t.optimize(ctx);
                let f = f.optimize(ctx);
                return ExprCompiled::if_expr(cond, t, f);
            }
            ExprCompiled::Slice(v_start_stop_step) => {
                let (v, start, stop, step) = &**v_start_stop_step;
                let v = v.optimize(ctx);
                let start = start.as_ref().map(|x| x.optimize(ctx));
                let stop = stop.as_ref().map(|x| x.optimize(ctx));
                let step = step.as_ref().map(|x| x.optimize(ctx));
                ExprCompiled::slice(span, v, start, stop, step, ctx)
            }
            ExprCompiled::Builtin1(op, e) => {
                let e = e.optimize(ctx);
                ExprCompiled::un_op(span, op, e, ctx)
            }
            ExprCompiled::LogicalBinOp(op, l_r) => {
                let (l, r) = &**l_r;
                let l = l.optimize(ctx);
                let r = r.optimize(ctx);
                return ExprCompiled::logical_bin_op(*op, l, r);
            }
            ExprCompiled::Seq(l_r) => {
                let (l, r) = &**l_r;
                let l = l.optimize(ctx);
                let r = r.optimize(ctx);
                return ExprCompiled::seq(l, r);
            }
            ExprCompiled::Builtin2(op, l_r) => {
                let (l, r) = &**l_r;
                let l = l.optimize(ctx);
                let r = r.optimize(ctx);
                ExprCompiled::bin_op(*op, l, r, ctx)
            }
            ExprCompiled::Index2(a_i0_i1) => {
                let (a, i0, i1) = &**a_i0_i1;
                let a = a.optimize(ctx);
                let i0 = i0.optimize(ctx);
                let i1 = i1.optimize(ctx);
                ExprCompiled::index2(a, i0, i1)
            }
            d @ ExprCompiled::Def(..) => (*d).clone(),
            ExprCompiled::Call(ref call) => call.optimize(ctx),
        };
        IrSpanned { node: expr, span }
    }
}

impl ExprCompiled {
    fn equals(l: IrSpanned<ExprCompiled>, r: IrSpanned<ExprCompiled>) -> IrSpanned<ExprCompiled> {
        let span = l.span.merge(&r.span);
        if let (Some(l), Some(r)) = (l.as_value(), r.as_value()) {
            // If comparison fails, let it fail in runtime.
            if let Ok(r) = l.equals(r.to_value()) {
                return IrSpanned {
                    span,
                    node: ExprCompiled::Value(FrozenValue::new_bool(r)),
                };
            }
        }

        let (l, r) = match try_eval_type_is(l, r) {
            Ok(e) => return e,
            Err((l, r)) => (l, r),
        };

        let (r, l) = match try_eval_type_is(r, l) {
            Ok(e) => return e,
            Err((r, l)) => (r, l),
        };

        IrSpanned {
            span,
            node: ExprCompiled::Builtin2(Builtin2::Equals, Box::new((l, r))),
        }
    }

    pub(crate) fn not(span: FrameSpan, expr: IrSpanned<ExprCompiled>) -> IrSpanned<ExprCompiled> {
        match expr.node {
            ExprCompiled::Value(x) => IrSpanned {
                node: ExprCompiled::Value(FrozenValue::new_bool(!x.to_value().to_bool())),
                span,
            },
            // Collapse `not not e` to `e` only if `e` is known to produce a boolean.
            ExprCompiled::Builtin1(Builtin1::Not, e) if e.is_definitely_bool() => (*e).clone(),
            _ => IrSpanned {
                node: ExprCompiled::Builtin1(Builtin1::Not, Box::new(expr)),
                span,
            },
        }
    }

    fn or(l: IrSpanned<ExprCompiled>, r: IrSpanned<ExprCompiled>) -> IrSpanned<ExprCompiled> {
        Self::logical_bin_op(ExprLogicalBinOp::Or, l, r)
    }

    fn and(l: IrSpanned<ExprCompiled>, r: IrSpanned<ExprCompiled>) -> IrSpanned<ExprCompiled> {
        Self::logical_bin_op(ExprLogicalBinOp::And, l, r)
    }

    pub(crate) fn logical_bin_op(
        op: ExprLogicalBinOp,
        l: IrSpanned<ExprCompiled>,
        r: IrSpanned<ExprCompiled>,
    ) -> IrSpanned<ExprCompiled> {
        if let Some(l_v) = l.is_pure_infallible_to_bool() {
            if l_v == (op == ExprLogicalBinOp::Or) {
                l
            } else {
                r
            }
        } else {
            let span = l.span.merge(&r.span);
            IrSpanned {
                node: ExprCompiled::LogicalBinOp(op, Box::new((l, r))),
                span,
            }
        }
    }

    pub(crate) fn seq(
        l: IrSpanned<ExprCompiled>,
        r: IrSpanned<ExprCompiled>,
    ) -> IrSpanned<ExprCompiled> {
        if l.is_pure_infallible() {
            r
        } else {
            let span = l.span.merge(&r.span);
            IrSpanned {
                node: ExprCompiled::Seq(Box::new((l, r))),
                span,
            }
        }
    }

    fn percent(
        l: IrSpanned<ExprCompiled>,
        r: IrSpanned<ExprCompiled>,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        if let Some(v) = l.as_string() {
            if let Some((before, after)) = parse_percent_s_one(&v) {
                let before = ctx.frozen_heap().alloc_str(&before);
                let after = ctx.frozen_heap().alloc_str(&after);
                return ExprCompiled::percent_s_one(before, r, after, ctx);
            }
        }
        ExprCompiled::Builtin2(Builtin2::Percent, Box::new((l, r)))
    }

    fn percent_s_one(
        before: FrozenStringValue,
        arg: IrSpanned<ExprCompiled>,
        after: FrozenStringValue,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        if let Some(arg) = arg.as_value() {
            if let Ok(value) =
                percent_s_one(before.as_str(), arg.to_value(), after.as_str(), ctx.heap())
            {
                let value = ctx.frozen_heap().alloc_str(value.as_str());
                return ExprCompiled::Value(value.to_frozen_value());
            }
        }

        ExprCompiled::Builtin1(Builtin1::PercentSOne(before, after), Box::new(arg))
    }

    pub(crate) fn format_one(
        before: FrozenStringValue,
        arg: IrSpanned<ExprCompiled>,
        after: FrozenStringValue,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        if let Some(arg) = arg.as_value() {
            let value = format_one(&before, arg.to_value(), &after, ctx.heap());
            let value = ctx.frozen_heap().alloc_str(value.as_str());
            return ExprCompiled::Value(value.to_frozen_value());
        }

        ExprCompiled::Builtin1(Builtin1::FormatOne(before, after), Box::new(arg))
    }

    fn add(l: IrSpanned<ExprCompiled>, r: IrSpanned<ExprCompiled>) -> ExprCompiled {
        let span = l.span.merge(&r.span);
        if let (Some(l), Some(r)) = (l.as_short_list_of_consts(), r.as_short_list_of_consts()) {
            let lr = l
                .iter()
                .chain(r.iter())
                .map(|x| IrSpanned {
                    node: ExprCompiled::Value(*x),
                    span,
                })
                .collect();
            return ExprCompiled::List(lr);
        }
        ExprCompiled::Builtin2(Builtin2::Add, Box::new((l, r)))
    }

    pub(crate) fn bin_op(
        bin_op: Builtin2,
        l: IrSpanned<ExprCompiled>,
        r: IrSpanned<ExprCompiled>,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        let span = l.span.merge(&r.span);
        // Binary operators should have no side effects,
        // but to avoid possible problems, we only fold binary operators on builtin types.
        if let (Some(l), Some(r)) = (l.as_builtin_value(), r.as_builtin_value()) {
            if let Ok(v) = bin_op.eval(l.to_value(), r.to_value(), ctx.heap()) {
                if let Some(v) = ExprCompiled::try_value(span, v, ctx.frozen_heap()) {
                    return v;
                }
            }
        }

        match bin_op {
            Builtin2::Percent => ExprCompiled::percent(l, r, ctx),
            Builtin2::Add => ExprCompiled::add(l, r),
            Builtin2::Equals => ExprCompiled::equals(l, r).node,
            Builtin2::ArrayIndex => ExprCompiled::index(l, r, ctx),
            bin_op => ExprCompiled::Builtin2(bin_op, Box::new((l, r))),
        }
    }

    pub(crate) fn if_expr(
        cond: IrSpanned<ExprCompiled>,
        t: IrSpanned<ExprCompiled>,
        f: IrSpanned<ExprCompiled>,
    ) -> IrSpanned<ExprCompiled> {
        let cond_span = cond.span;
        let cond = ExprCompiledBool::new(cond);
        match cond.node {
            ExprCompiledBool::Const(true) => t,
            ExprCompiledBool::Const(false) => f,
            ExprCompiledBool::Expr(cond) => match cond {
                ExprCompiled::Builtin1(Builtin1::Not, cond) => ExprCompiled::if_expr(*cond, f, t),
                ExprCompiled::Seq(x_cond) => {
                    let (x, cond) = *x_cond;
                    ExprCompiled::seq(x, ExprCompiled::if_expr(cond, t, f))
                }
                cond => {
                    let cond = IrSpanned {
                        node: cond,
                        span: cond_span,
                    };
                    let span = cond.span.merge(&t.span).merge(&f.span);
                    IrSpanned {
                        node: ExprCompiled::If(Box::new((cond, t, f))),
                        span,
                    }
                }
            },
        }
    }

    pub(crate) fn un_op(
        span: FrameSpan,
        op: &Builtin1,
        expr: IrSpanned<ExprCompiled>,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        if let Some(v) = expr.as_builtin_value() {
            if let Some(v) = op.eval(v, ctx) {
                if let Some(v) = ExprCompiled::try_value(expr.span, v, ctx.frozen_heap()) {
                    return v;
                }
            }
        }
        match op {
            Builtin1::FormatOne(before, after) => {
                ExprCompiled::format_one(*before, expr, *after, ctx)
            }
            Builtin1::PercentSOne(before, after) => {
                ExprCompiled::percent_s_one(*before, expr, *after, ctx)
            }
            Builtin1::Dot(field) => ExprCompiled::dot(expr, field, ctx),
            Builtin1::TypeIs(t) => ExprCompiled::type_is(expr, *t),
            Builtin1::Not => ExprCompiled::not(span, expr).node,
            op => ExprCompiled::Builtin1(op.clone(), Box::new(expr)),
        }
    }

    fn try_values(
        span: FrameSpan,
        values: &[Value],
        heap: &FrozenHeap,
    ) -> Option<Vec<IrSpanned<ExprCompiled>>> {
        values
            .try_map(|v| {
                Self::try_value(span, *v, heap)
                    .map(|expr| IrSpanned { span, node: expr })
                    .ok_or(())
            })
            .ok()
    }

    /// Try convert a maybe not frozen value to an expression, or discard it.
    pub(crate) fn try_value(span: FrameSpan, v: Value, heap: &FrozenHeap) -> Option<ExprCompiled> {
        if let Some(v) = v.unpack_frozen() {
            // If frozen, we are lucky.
            Some(ExprCompiled::Value(v))
        } else if let Some(v) = v.unpack_str() {
            if v.len() <= 1000 {
                // If string, copy it to frozen heap.
                Some(ExprCompiled::Value(heap.alloc_str(v).to_frozen_value()))
            } else {
                // Long strings may lead to exponential explosion in the optimizer,
                // so skips optimizations for them.
                None
            }
        } else if let Some(v) = v.downcast_ref::<StarlarkFloat>() {
            Some(ExprCompiled::Value(heap.alloc(*v)))
        } else if let Some(v) = v.downcast_ref::<Range>() {
            Some(ExprCompiled::Value(heap.alloc(*v)))
        } else if let Some(v) = ListRef::from_value(v) {
            // When spec-safe function returned a non-frozen list,
            // we try to convert that list to a list of constants instruction.
            let items = Self::try_values(span, v.content(), heap)?;
            Some(ExprCompiled::List(items))
        } else if let Some(v) = Tuple::from_value(v) {
            let items = Self::try_values(span, v.content(), heap)?;
            Some(Self::tuple(items, heap))
        } else {
            None
        }
    }

    pub(crate) fn compr(compr: ComprCompiled) -> ExprCompiled {
        match compr {
            ComprCompiled::List(x, clauses) => {
                if clauses.is_nop() {
                    ExprCompiled::List(Vec::new())
                } else {
                    ExprCompiled::Compr(ComprCompiled::List(x, clauses))
                }
            }
            ComprCompiled::Dict(k_v, clauses) => {
                let (k, v) = *k_v;
                if clauses.is_nop() {
                    ExprCompiled::Dict(Vec::new())
                } else {
                    ExprCompiled::Compr(ComprCompiled::Dict(Box::new((k, v)), clauses))
                }
            }
        }
    }

    /// Construct tuple expression from elements optimizing to frozen tuple value when possible.
    pub(crate) fn tuple(elems: Vec<IrSpanned<ExprCompiled>>, heap: &FrozenHeap) -> ExprCompiled {
        if let Ok(elems) = elems.try_map(|e| e.as_value().ok_or(())) {
            ExprCompiled::Value(heap.alloc_tuple(&elems))
        } else {
            ExprCompiled::Tuple(elems)
        }
    }

    pub(crate) fn compile_time_getattr(
        left: FrozenValue,
        attr: &Symbol,
        ctx: &mut OptCtx,
    ) -> Option<FrozenValue> {
        // We assume `getattr` has no side effects.
        let v = get_attr_hashed_raw(left.to_value(), attr, ctx.heap()).ok()?;
        match v {
            MemberOrValue::Member(m) => match MaybeUnboundValue::new(m) {
                MaybeUnboundValue::Method(m) => {
                    Some(ctx.frozen_heap().alloc_simple(BoundMethodGen::new(left, m)))
                }
                MaybeUnboundValue::Attr(..) => None,
            },
            MemberOrValue::Value(v) => v.unpack_frozen(),
        }
    }

    pub(crate) fn dot(
        object: IrSpanned<ExprCompiled>,
        field: &Symbol,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        if let Some(left) = object.as_value() {
            if let Some(v) = Self::compile_time_getattr(left, field, ctx) {
                return ExprCompiled::Value(v);
            }
        }

        ExprCompiled::Builtin1(Builtin1::Dot(field.clone()), Box::new(object))
    }

    fn slice(
        span: FrameSpan,
        array: IrSpanned<ExprCompiled>,
        start: Option<IrSpanned<ExprCompiled>>,
        stop: Option<IrSpanned<ExprCompiled>>,
        step: Option<IrSpanned<ExprCompiled>>,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        if let (Some(array), Some(start), Some(stop), Some(step)) = (
            array.as_builtin_value(),
            start.as_ref().map(|e| e.as_value()),
            stop.as_ref().map(|e| e.as_value()),
            step.as_ref().map(|e| e.as_value()),
        ) {
            if let Ok(v) = array.to_value().slice(
                start.map(|v| v.to_value()),
                stop.map(|v| v.to_value()),
                step.map(|v| v.to_value()),
                ctx.heap(),
            ) {
                if let Some(v) = ExprCompiled::try_value(span, v, ctx.frozen_heap()) {
                    return v;
                }
            }
        }
        ExprCompiled::Slice(Box::new((array, start, stop, step)))
    }

    pub(crate) fn index(
        array: IrSpanned<ExprCompiled>,
        index: IrSpanned<ExprCompiled>,
        ctx: &mut OptCtx,
    ) -> ExprCompiled {
        let span = array.span.merge(&index.span);
        if let (Some(array), Some(index)) = (array.as_builtin_value(), index.as_value()) {
            if let Ok(v) = array.to_value().at(index.to_value(), ctx.heap()) {
                if let Some(expr) = ExprCompiled::try_value(span, v, ctx.frozen_heap()) {
                    return expr;
                }
            }
        }
        ExprCompiled::Builtin2(Builtin2::ArrayIndex, Box::new((array, index)))
    }

    pub(crate) fn index2(
        array: IrSpanned<ExprCompiled>,
        index0: IrSpanned<ExprCompiled>,
        index1: IrSpanned<ExprCompiled>,
    ) -> ExprCompiled {
        ExprCompiled::Index2(Box::new((array, index0, index1)))
    }

    pub(crate) fn typ(span: FrameSpan, v: IrSpanned<ExprCompiled>) -> ExprCompiled {
        match &v.node {
            ExprCompiled::Value(v) => {
                ExprCompiled::Value(v.to_value().get_type_value().to_frozen_value())
            }
            ExprCompiled::Tuple(xs) if xs.iter().all(|e| e.is_pure_infallible()) => {
                ExprCompiled::Value(Tuple::get_type_value_static().to_frozen_value())
            }
            ExprCompiled::List(xs) if xs.iter().all(|e| e.is_pure_infallible()) => {
                ExprCompiled::Value(ListData::get_type_value_static().to_frozen_value())
            }
            ExprCompiled::Dict(xs) if xs.is_empty() => {
                ExprCompiled::Value(Dict::get_type_value_static().to_frozen_value())
            }
            ExprCompiled::Builtin1(Builtin1::Not | Builtin1::TypeIs(_), x)
                if x.is_pure_infallible() =>
            {
                ExprCompiled::Value(StarlarkBool::get_type_value_static().to_frozen_value())
            }
            _ => ExprCompiled::Call(Box::new(IrSpanned {
                span,
                node: CallCompiled {
                    fun: IrSpanned {
                        span,
                        node: ExprCompiled::Value(Constants::get().fn_type.0),
                    },
                    args: ArgsCompiledValue {
                        pos_named: vec![v],
                        ..ArgsCompiledValue::default()
                    },
                },
            })),
        }
    }

    pub(crate) fn type_is(v: IrSpanned<ExprCompiled>, t: FrozenStringValue) -> ExprCompiled {
        if let Some(v) = v.as_value() {
            return ExprCompiled::Value(FrozenValue::new_bool(
                v.to_value().get_type() == t.as_str(),
            ));
        }
        ExprCompiled::Builtin1(Builtin1::TypeIs(t), Box::new(v))
    }

    pub(crate) fn len(span: FrameSpan, arg: IrSpanned<ExprCompiled>) -> ExprCompiled {
        if let Some(arg) = arg.as_value() {
            if let Ok(len) = arg.to_value().length() {
                if let Ok(len) = InlineInt::try_from(len) {
                    return ExprCompiled::Value(FrozenValue::new_int(len));
                }
            }
        }
        ExprCompiled::Call(Box::new(IrSpanned {
            span,
            node: CallCompiled {
                fun: IrSpanned {
                    span,
                    node: ExprCompiled::Value(Constants::get().fn_len.0),
                },
                args: ArgsCompiledValue {
                    pos_named: vec![arg],
                    ..ArgsCompiledValue::default()
                },
            },
        }))
    }
}

#[derive(Debug, Clone, Error)]
pub(crate) enum EvalError {
    #[error("Dictionary key repeated for `{0}`")]
    DuplicateDictionaryKey(String),
}

/// Try fold expression `cmp(l == r)` into `cmp(type(x) == "y")`.
/// Return original `l` and `r` arguments if fold was unsuccessful.
fn try_eval_type_is(
    l: IrSpanned<ExprCompiled>,
    r: IrSpanned<ExprCompiled>,
) -> Result<IrSpanned<ExprCompiled>, (IrSpanned<ExprCompiled>, IrSpanned<ExprCompiled>)> {
    let span = l.span.merge(&r.span);
    if let (Some(l), Some(r)) = (l.as_type(), r.as_string()) {
        Ok(IrSpanned {
            span,
            node: ExprCompiled::type_is(l.clone(), r),
        })
    } else {
        Err((l, r))
    }
}

impl AstLiteral {
    fn compile(&self, heap: &FrozenHeap) -> FrozenValue {
        match self {
            AstLiteral::Int(i) => heap.alloc(i.node.0.clone()),
            AstLiteral::Float(f) => heap.alloc(f.node),
            AstLiteral::String(x) => heap.alloc(x.node.as_str()),
        }
    }
}

impl<P: AstPayload> ExprP<P> {
    fn unpack_string_literal(&self) -> Option<&str> {
        match self {
            ExprP::Literal(AstLiteral::String(i)) => Some(&i.node),
            _ => None,
        }
    }

    // Does an entire sequence of additions reduce to a string literal
    fn reduces_to_string<'a>(
        mut op: BinOp,
        mut left: &'a AstExprP<P>,
        mut right: &'a AstExprP<P>,
    ) -> Option<String> {
        let mut results = Vec::new();
        loop {
            if op != BinOp::Add {
                return None;
            }
            // a + b + c  associates as  (a + b) + c
            let x = right.unpack_string_literal()?;
            results.push(x.to_owned());
            match &left.node {
                ExprP::Op(left2, op2, right2) => {
                    op = *op2;
                    left = left2;
                    right = right2;
                }
                _ => {
                    let x = left.unpack_string_literal()?;
                    results.push(x.to_owned());
                    break;
                }
            }
        }
        results.reverse();
        Some(results.concat())
    }
}

#[cold]
#[inline(never)]
fn get_attr_no_attr_error<'v>(x: Value<'v>, attribute: &Symbol) -> anyhow::Error {
    match did_you_mean(attribute.as_str(), x.dir_attr().iter().map(|s| s.as_str())) {
        None => ValueError::NoAttr(x.get_type().to_owned(), attribute.as_str().to_owned()).into(),
        Some(better) => ValueError::NoAttrDidYouMean(
            x.get_type().to_owned(),
            attribute.as_str().to_owned(),
            better.to_owned(),
        )
        .into(),
    }
}

pub(crate) enum MemberOrValue<'v> {
    Member(FrozenValueNotSpecial),
    Value(Value<'v>),
}

impl<'v> MemberOrValue<'v> {
    pub(crate) fn to_value(&self) -> Value<'v> {
        match self {
            MemberOrValue::Member(x) => x.to_value(),
            MemberOrValue::Value(x) => *x,
        }
    }
}

#[inline(always)]
pub(crate) fn get_attr_hashed_raw<'v>(
    x: Value<'v>,
    attribute: &Symbol,
    heap: &'v Heap,
) -> anyhow::Result<MemberOrValue<'v>> {
    let aref = x.get_ref();
    if let Some(methods) = aref.get_methods() {
        if let Some(v) = methods.get_frozen_symbol(attribute) {
            return Ok(MemberOrValue::Member(v));
        }
    }
    match aref.get_attr_hashed(attribute.as_str_hashed(), heap) {
        None => Err(get_attr_no_attr_error(x, attribute)),
        Some(x) => Ok(MemberOrValue::Value(x)),
    }
}

pub(crate) fn get_attr_hashed_bind<'v>(
    x: Value<'v>,
    attribute: &Symbol,
    heap: &'v Heap,
) -> anyhow::Result<Value<'v>> {
    let aref = x.get_ref();
    if let Some(methods) = aref.get_methods() {
        if let Some(v) = methods.get_frozen_symbol(attribute) {
            return MaybeUnboundValue::new(v).bind(x, heap);
        }
    }
    match aref.get_attr_hashed(attribute.as_str_hashed(), heap) {
        None => Err(get_attr_no_attr_error(x, attribute)),
        Some(x) => {
            // Only `get_methods` is allowed to return unbound methods,
            // so we assume the value is bound here.
            // TODO(nga): if `NativeMethod` or `NativeAttribute` is returned from `get_attr`,
            //   we get an inconsistency between handling of unbound objects
            //   in various call sites in the crate.
            //   However, `NativeMethod` and `NativeAttribute` are not actually useful
            //   as Starlark values, and can be hidden from public API and can be made
            //   to never appear as user-visible values.
            //   Do that.
            //   Alternatively, allow `NativeMethod` and `NativeAttribute` here,
            //   but that would have negative performance implications.
            Ok(x)
        }
    }
}

impl<'v, 'a, 'e> Compiler<'v, 'a, 'e> {
    fn expr_ident(&mut self, ident: &CstIdent) -> ExprCompiled {
        let resolved_ident = ident
            .node
            .1
            .as_ref()
            .unwrap_or_else(|| panic!("variable not resolved: `{}`", ident.node.0));
        match resolved_ident {
            ResolvedIdent::Slot(Slot::Local(slot), binding_id) => {
                let binding = self.scope_data.get_binding(*binding_id);

                // We can't look up the local variabless in advance, because they are different each time
                // we go through a new function call.
                match binding.captured {
                    Captured::Yes => ExprCompiled::LocalCaptured(LocalCapturedSlotId(slot.0)),
                    Captured::No => ExprCompiled::Local(LocalSlotId(slot.0)),
                }
            }
            ResolvedIdent::Slot(Slot::Module(slot), binding_id) => {
                let binding = self.scope_data.get_binding(*binding_id);

                // We can only inline variables if they were assigned once
                // otherwise we might inline the wrong value.
                if binding.assign_count == AssignCount::AtMostOnce {
                    if let Some(v) = self.eval.module_env.slots().get_slot(*slot) {
                        // We could inline non-frozen values, but these values
                        // can be garbage-collected, so it is somewhat harder to implement.
                        if let Some(v) = v.unpack_frozen() {
                            return ExprCompiled::Value(v);
                        }
                    }
                }

                ExprCompiled::Module(*slot)
            }
            ResolvedIdent::Global(v) => ExprCompiled::Value(*v),
        }
    }

    fn opt_ctx<'s>(&'s mut self) -> OptCtx<'v, 'a, 's> {
        let param_count = self.current_scope().param_count();
        OptCtx::new(self.eval, param_count)
    }

    pub(crate) fn expr(&mut self, expr: &CstExpr) -> IrSpanned<ExprCompiled> {
        // println!("compile {}", expr.node);
        let span = FrameSpan::new(FrozenFileSpan::new(self.codemap, expr.span));
        let expr = match &expr.node {
            ExprP::Identifier(ident) => self.expr_ident(ident),
            ExprP::Lambda(l) => {
                let signature_span = l.signature_span();
                let signature_span = FrozenFileSpan::new(self.codemap, signature_span);
                let LambdaP {
                    params,
                    body,
                    payload: scope_id,
                } = l;
                let suite = Spanned {
                    span: expr.span,
                    // TODO(nga): unnecessary clone.
                    node: StmtP::Return(Some(*body.clone())),
                };
                self.function("lambda", signature_span, *scope_id, params, None, &suite)
            }
            ExprP::Tuple(exprs) => {
                let xs = exprs.map(|x| self.expr(x));
                ExprCompiled::tuple(xs, self.eval.module_env.frozen_heap())
            }
            ExprP::List(exprs) => {
                let xs = exprs.map(|x| self.expr(x));
                ExprCompiled::List(xs)
            }
            ExprP::Dict(exprs) => {
                let xs = exprs.map(|(k, v)| (self.expr(k), self.expr(v)));
                ExprCompiled::Dict(xs)
            }
            ExprP::If(cond_then_expr_else_expr) => {
                let (cond, then_expr, else_expr) = &**cond_then_expr_else_expr;
                let cond = self.expr(cond);
                let then_expr = self.expr(then_expr);
                let else_expr = self.expr(else_expr);
                return ExprCompiled::if_expr(cond, then_expr, else_expr);
            }
            ExprP::Dot(left, right) => {
                let left = self.expr(left);
                let s = Symbol::new(&right.node);

                ExprCompiled::dot(left, &s, &mut self.opt_ctx())
            }
            ExprP::Call(left, args) => {
                let left = self.expr(left);
                let args = self.args(args);
                CallCompiled::call(span, left, args, &mut self.opt_ctx())
            }
            ExprP::Index(array_index) => {
                let (array, index) = &**array_index;
                let array = self.expr(array);
                let index = self.expr(index);
                ExprCompiled::index(array, index, &mut self.opt_ctx())
            }
            ExprP::Index2(array_index0_index1) => {
                let (array, index0, index1) = &**array_index0_index1;
                let array = self.expr(array);
                let index0 = self.expr(index0);
                let index1 = self.expr(index1);
                ExprCompiled::index2(array, index0, index1)
            }
            ExprP::Slice(collection, start, stop, stride) => {
                let collection = self.expr(collection);
                let start = start.as_ref().map(|x| self.expr(x));
                let stop = stop.as_ref().map(|x| self.expr(x));
                let stride = stride.as_ref().map(|x| self.expr(x));
                ExprCompiled::slice(span, collection, start, stop, stride, &mut self.opt_ctx())
            }
            ExprP::Not(expr) => {
                let expr = self.expr(expr);
                return ExprCompiled::not(span, expr);
            }
            ExprP::Minus(expr) => {
                let expr = self.expr(expr);
                ExprCompiled::un_op(span, &Builtin1::Minus, expr, &mut self.opt_ctx())
            }
            ExprP::Plus(expr) => {
                let expr = self.expr(expr);
                ExprCompiled::un_op(span, &Builtin1::Plus, expr, &mut self.opt_ctx())
            }
            ExprP::BitNot(expr) => {
                let expr = self.expr(expr);
                ExprCompiled::un_op(span, &Builtin1::BitNot, expr, &mut self.opt_ctx())
            }
            ExprP::Op(left, op, right) => {
                if let Some(x) = ExprP::reduces_to_string(*op, left, right) {
                    // Note there's const propagation for `+` on compiled expressions,
                    // but special handling of `+` on AST might be slightly more efficient
                    // (no unnecessary allocations on the heap). So keep it.
                    let val = self.eval.module_env.frozen_heap().alloc(x);
                    ExprCompiled::Value(val)
                } else {
                    let right = if *op == BinOp::In || *op == BinOp::NotIn {
                        list_to_tuple(right)
                    } else {
                        Cow::Borrowed(&**right)
                    };

                    let l = self.expr(left);
                    let r = self.expr(&right);
                    match op {
                        BinOp::Or => return ExprCompiled::or(l, r),
                        BinOp::And => return ExprCompiled::and(l, r),
                        BinOp::Equal => return ExprCompiled::equals(l, r),
                        BinOp::NotEqual => {
                            return ExprCompiled::not(span, ExprCompiled::equals(l, r));
                        }
                        BinOp::Less => ExprCompiled::bin_op(
                            Builtin2::Compare(CompareOp::Less),
                            l,
                            r,
                            &mut self.opt_ctx(),
                        ),
                        BinOp::Greater => ExprCompiled::bin_op(
                            Builtin2::Compare(CompareOp::Greater),
                            l,
                            r,
                            &mut self.opt_ctx(),
                        ),
                        BinOp::LessOrEqual => ExprCompiled::bin_op(
                            Builtin2::Compare(CompareOp::LessOrEqual),
                            l,
                            r,
                            &mut self.opt_ctx(),
                        ),
                        BinOp::GreaterOrEqual => ExprCompiled::bin_op(
                            Builtin2::Compare(CompareOp::GreaterOrEqual),
                            l,
                            r,
                            &mut self.opt_ctx(),
                        ),
                        BinOp::In => ExprCompiled::bin_op(Builtin2::In, l, r, &mut self.opt_ctx()),
                        BinOp::NotIn => {
                            ExprCompiled::not(
                                span,
                                IrSpanned {
                                    span,
                                    node: ExprCompiled::bin_op(
                                        Builtin2::In,
                                        l,
                                        r,
                                        &mut self.opt_ctx(),
                                    ),
                                },
                            )
                            .node
                        }
                        BinOp::Subtract => {
                            ExprCompiled::bin_op(Builtin2::Sub, l, r, &mut self.opt_ctx())
                        }
                        BinOp::Add => {
                            ExprCompiled::bin_op(Builtin2::Add, l, r, &mut self.opt_ctx())
                        }
                        BinOp::Multiply => {
                            ExprCompiled::bin_op(Builtin2::Multiply, l, r, &mut self.opt_ctx())
                        }
                        BinOp::Percent => {
                            ExprCompiled::bin_op(Builtin2::Percent, l, r, &mut self.opt_ctx())
                        }
                        BinOp::Divide => {
                            ExprCompiled::bin_op(Builtin2::Divide, l, r, &mut self.opt_ctx())
                        }
                        BinOp::FloorDivide => {
                            ExprCompiled::bin_op(Builtin2::FloorDivide, l, r, &mut self.opt_ctx())
                        }
                        BinOp::BitAnd => {
                            ExprCompiled::bin_op(Builtin2::BitAnd, l, r, &mut self.opt_ctx())
                        }
                        BinOp::BitOr => {
                            ExprCompiled::bin_op(Builtin2::BitOr, l, r, &mut self.opt_ctx())
                        }
                        BinOp::BitXor => {
                            ExprCompiled::bin_op(Builtin2::BitXor, l, r, &mut self.opt_ctx())
                        }
                        BinOp::LeftShift => {
                            ExprCompiled::bin_op(Builtin2::LeftShift, l, r, &mut self.opt_ctx())
                        }
                        BinOp::RightShift => {
                            ExprCompiled::bin_op(Builtin2::RightShift, l, r, &mut self.opt_ctx())
                        }
                    }
                }
            }
            ExprP::ListComprehension(x, for_, clauses) => self.list_comprehension(x, for_, clauses),
            ExprP::DictComprehension(k_v, for_, clauses) => {
                let (k, v) = &**k_v;
                self.dict_comprehension(k, v, for_, clauses)
            }
            ExprP::Literal(x) => {
                let val = x.compile(self.eval.module_env.frozen_heap());
                ExprCompiled::Value(val)
            }
        };
        IrSpanned { node: expr, span }
    }

    /// Like `expr` but returns an expression optimized assuming
    /// only the truth of the result is needed.
    pub(crate) fn expr_truth(&mut self, expr: &CstExpr) -> IrSpanned<ExprCompiledBool> {
        let expr = self.expr(expr);
        ExprCompiledBool::new(expr)
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Boolean expression.

use crate::eval::compiler::expr::Builtin1;
use crate::eval::compiler::expr::ExprCompiled;
use crate::eval::compiler::expr::ExprLogicalBinOp;
use crate::eval::compiler::span::IrSpanned;
use crate::eval::runtime::frame_span::FrameSpan;
use crate::values::FrozenValue;

/// Boolean expression.
pub(crate) enum ExprCompiledBool {
    Const(bool),
    /// Non-const expression.
    Expr(ExprCompiled),
}

impl IrSpanned<ExprCompiledBool> {
    pub(crate) fn into_expr(self) -> IrSpanned<ExprCompiled> {
        IrSpanned {
            span: self.span,
            node: self.node.into_expr(),
        }
    }
}

impl ExprCompiledBool {
    fn into_expr(self) -> ExprCompiled {
        match self {
            ExprCompiledBool::Const(b) => ExprCompiled::Value(FrozenValue::new_bool(b)),
            ExprCompiledBool::Expr(e) => e,
        }
    }

    fn const_value(&self) -> Option<bool> {
        match self {
            ExprCompiledBool::Const(b) => Some(*b),
            ExprCompiledBool::Expr(..) => None,
        }
    }

    /// `bool(x)` and do trivial optimizations.
    pub(crate) fn new(expr: IrSpanned<ExprCompiled>) -> IrSpanned<ExprCompiledBool> {
        fn new_bool(span: FrameSpan, b: bool) -> IrSpanned<ExprCompiledBool> {
            IrSpanned {
                node: ExprCompiledBool::Const(b),
                span,
            }
        }

        let span = expr.span;

        if let Some(b) = expr.is_pure_infallible_to_bool() {
            return new_bool(span, b);
        }

        match expr.node {
            ExprCompiled::Builtin1(Builtin1::Not, x) => {
                let x = Self::new(*x);
                match x.const_value() {
                    Some(b) => new_bool(span, !b),
                    None => IrSpanned {
                        node: ExprCompiledBool::Expr(ExprCompiled::Builtin1(
                            Builtin1::Not,
                            Box::new(x.into_expr()),
                        )),
                        span,
                    },
                }
            }
            ExprCompiled::LogicalBinOp(op, x_y) => {
                let (x, y) = *x_y;
                let x = Self::new(x);
                let y = Self::new(y);
                match (op, x.const_value(), y.const_value()) {
                    (ExprLogicalBinOp::And, Some(false), _) => new_bool(span, false),
                    (ExprLogicalBinOp::Or, Some(true), _) => new_bool(span, true),
                    (ExprLogicalBinOp::And, Some(true), _) => y,
                    (ExprLogicalBinOp::Or, Some(false), _) => y,
                    (ExprLogicalBinOp::And, None, Some(true)) => x,
                    (ExprLogicalBinOp::Or, None, Some(false)) => x,
                    (ExprLogicalBinOp::And, None, Some(false)) => {
                        // The expression evaluates to false,
                        // but we need to preserve LHS for the effect.
                        IrSpanned {
                            span,
                            node: ExprCompiledBool::Expr(
                                ExprCompiled::seq(
                                    x.into_expr(),
                                    new_bool(y.span, false).into_expr(),
                                )
                                .node,
                            ),
                        }
                    }
                    (ExprLogicalBinOp::Or, None, Some(true)) => {
                        // The expression evaluates to true,
                        // but we need to preserve LHS for the effect.
                        IrSpanned {
                            span,
                            node: ExprCompiledBool::Expr(
                                ExprCompiled::seq(
                                    x.into_expr(),
                                    new_bool(y.span, true).into_expr(),
                                )
                                .node,
                            ),
                        }
                    }
                    (op, None, None) => IrSpanned {
                        node: ExprCompiledBool::Expr(ExprCompiled::LogicalBinOp(
                            op,
                            Box::new((x.into_expr(), y.into_expr())),
                        )),
                        span,
                    },
                }
            }
            expr => IrSpanned {
                node: ExprCompiledBool::Expr(expr),
                span,
            },
        }
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Things that operate on known values where we know we can do better.

use std::borrow::Cow;

use crate::codemap::Spanned;
use crate::eval::compiler::scope::payload::CstExpr;
use crate::syntax::ast::ExprP;

/// Convert a list into a tuple. In many cases (iteration, `in`) these types
/// behave the same, but a list has identity and mutability, so much better to
/// switch to tuple where it makes no difference. A tuple of constants
/// will go on the FrozenHeap, while a list of constants will be continually
/// reallocated.
pub(crate) fn list_to_tuple(x: &CstExpr) -> Cow<CstExpr> {
    match x {
        Spanned {
            node: ExprP::List(x),
            span,
        } => Cow::Owned(Spanned {
            // TODO(nga): do not clone.
            node: ExprP::Tuple(x.clone()),
            span: *span,
        }),
        _ => Cow::Borrowed(x),
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

pub(crate) mod args;
pub(crate) mod call;
pub(crate) mod compr;
pub(crate) mod constants;
pub(crate) mod def;
pub(crate) mod def_inline;
pub(crate) mod expr;
pub(crate) mod expr_bool;
pub(crate) mod known;
pub(crate) mod module;
pub(crate) mod opt_ctx;
pub(crate) mod scope;
pub(crate) mod small_vec_1;
pub(crate) mod span;
pub(crate) mod stmt;
pub(crate) mod types;

use std::fmt::Debug;

use crate::codemap::CodeMap;
use crate::codemap::Span;
use crate::environment::Globals;
use crate::errors::Diagnostic;
use crate::eval::compiler::scope::ModuleScopeData;
use crate::eval::compiler::scope::ScopeId;
use crate::eval::compiler::scope::ScopeNames;
use crate::eval::compiler::scope::TopLevelStmtIndex;
use crate::eval::runtime::frame_span::FrameSpan;
use crate::eval::Evaluator;
use crate::values::FrozenRef;

/// Error with location.
#[derive(Debug, derive_more::Display)]
// TODO(nga): lalrpop generates public members which require error type to be public too.
#[doc(hidden)]
pub struct EvalException(
    /// Error is `Diagnostic`, but stored as `anyhow::Error` for smaller size.
    anyhow::Error,
);

impl EvalException {
    #[cold]
    pub(crate) fn into_anyhow(self) -> anyhow::Error {
        self.0
    }

    #[cold]
    pub(crate) fn new(error: anyhow::Error, span: Span, codemap: &CodeMap) -> EvalException {
        EvalException(Diagnostic::new(error, span, codemap))
    }

    pub(crate) fn _testing_loc(mut err: &anyhow::Error) -> crate::codemap::ResolvedFileSpan {
        if let Some(eval_exc) = err.downcast_ref::<EvalException>() {
            err = &eval_exc.0;
        }
        match err.downcast_ref::<Diagnostic>() {
            Some(d) => d.span.as_ref().unwrap().resolve(),
            None => panic!("Expected Diagnostic, got {:#?}", err),
        }
    }
}

#[cold]
#[inline(never)]
fn add_span_to_error(e: anyhow::Error, span: FrameSpan, eval: &Evaluator) -> anyhow::Error {
    Diagnostic::modify(e, |d: &mut Diagnostic| {
        d.set_span(span.span.span(), &span.span.file());
        d.set_call_stack(|| eval.call_stack.to_diagnostic_frames(span.inlined_frames));
    })
}

#[cold]
#[inline(never)]
pub(crate) fn add_span_to_expr_error(
    e: anyhow::Error,
    span: FrameSpan,
    eval: &Evaluator,
) -> EvalException {
    EvalException(add_span_to_error(e, span, eval))
}

/// Convert syntax error to spanned evaluation exception
#[inline(always)]
pub(crate) fn expr_throw<'v, T>(
    r: anyhow::Result<T>,
    span: FrameSpan,
    eval: &Evaluator<'v, '_>,
) -> Result<T, EvalException> {
    match r {
        Ok(v) => Ok(v),
        Err(e) => Err(add_span_to_expr_error(e, span, eval)),
    }
}

pub(crate) struct Compiler<'v, 'a, 'e> {
    pub(crate) eval: &'e mut Evaluator<'v, 'a>,
    pub(crate) scope_data: ModuleScopeData<'v>,
    pub(crate) locals: Vec<ScopeId>,
    pub(crate) globals: FrozenRef<'static, Globals>,
    pub(crate) codemap: FrozenRef<'static, CodeMap>,
    pub(crate) check_types: bool,
    pub(crate) top_level_stmt_count: usize,
    pub(crate) last_stmt_defining_type: Option<TopLevelStmtIndex>,
    /// Last statement with types populated into payload.
    pub(crate) last_stmt_with_populated_types: TopLevelStmtIndex,
}

impl Compiler<'_, '_, '_> {
    pub(crate) fn enter_scope(&mut self, scope_id: ScopeId) {
        self.locals.push(scope_id);
    }

    pub(crate) fn exit_scope(&mut self) -> ScopeId {
        self.locals.pop().unwrap()
    }

    pub(crate) fn current_scope(&self) -> &ScopeNames {
        self.scope_data.get_scope(*self.locals.last().unwrap())
    }
}


/*
 * Copyright 2019 The Starlark in Rust Authors.
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! Compile and evaluate module top-level statements.

use std::slice;

use crate::codemap::Spanned;
use crate::const_frozen_string;
use crate::eval::bc::frame::alloca_frame;
use crate::eval::compiler::add_span_to_expr_error;
use crate::eval::compiler::expr_throw;
use crate::eval::compiler::scope::payload::CstPayload;
use crate::eval::compiler::scope::payload::CstStmt;
use crate::eval::compiler::scope::ScopeId;
use crate::eval::compiler::scope::Slot;
use crate::eval::compiler::scope::TopLevelStmtIndex;
use crate::eval::compiler::Compiler;
use crate::eval::compiler::EvalException;
use crate::eval::runtime::frame_span::FrameSpan;
use crate::eval::runtime::frozen_file_span::FrozenFileSpan;
use crate::syntax::ast::LoadP;
use crate::syntax::ast::StmtP;
use crate::typing::bindings::BindingsCollect;
use crate::typing::error::InternalError;
use crate::typing::mode::TypecheckMode;
use crate::typing::oracle::traits::OracleAny;
use crate::typing::typecheck::solve_bindings;
use crate::values::FrozenRef;
use crate::values::FrozenStringValue;
use crate::values::Value;

#[derive(Debug, thiserror::Error)]
enum ModuleError {
    #[error("No imports are available, you tried `{0}` (no call to `Evaluator.set_loader`)")]
    NoImportsAvailable(String),
    #[error("Unexpected statement (internal error)")]
    UnexpectedStatement,
    #[error("Top level stmt count mismatch (internal error)")]
    TopLevelStmtCountMismatch,
}

impl<'v> Compiler<'v, '_, '_> {
    fn eval_load(&mut self, load: Spanned<&LoadP<CstPayload>>) -> Result<(), EvalException> {
        let name = &load.node.module.node;

        let span = FrameSpan::new(FrozenFileSpan::new(self.codemap, load.span));

        let loadenv = match self.eval.loader.as_ref() {
            None => {
                return Err(add_span_to_expr_error(
                    ModuleError::NoImportsAvailable(name.to_owned()).into(),
                    span,
                    self.eval,
                ));
            }
            Some(loader) => expr_throw(loader.load(&name), span, self.eval)?,
        };

        for (our_name, their_name) in &load.node.args {
            let (slot, _captured) = self.scope_data.get_assign_ident_slot(&our_name);
            let slot = match slot {
                Slot::Local(..) => unreachable!("symbol need to be resolved to module"),
                Slot::Module(slot) => slot,
            };
            let value = expr_throw(
                self.eval.module_env.load_symbol(&loadenv, &their_name.node),
                FrameSpan::new(FrozenFileSpan::new(
                    self.codemap,
                    our_name.span.merge(their_name.span),
                )),
                self.eval,
            )?;
            self.eval.set_slot_module(slot, value)
        }

        Ok(())
    }

    /// Compile and evaluate regular statement.
    /// Regular statement is a statement which is not `load` or a sequence of statements.
    fn eval_regular_top_level_stmt(
        &mut self,
        stmt: &mut CstStmt,
        local_names: FrozenRef<'static, [FrozenStringValue]>,
    ) -> Result<Value<'v>, EvalException> {
        if matches!(stmt.node, StmtP::Statements(_) | StmtP::Load(_)) {
            return Err(EvalException::new(
                ModuleError::UnexpectedStatement.into(),
                stmt.span,
                &self.codemap,
            ));
        }

        let stmt = self.module_top_level_stmt(stmt);
        let bc = stmt.as_bc(
            &self.compile_context(false),
            local_names,
            0,
            self.eval.module_env.frozen_heap(),
        );
        // We don't preserve locals between top level statements.
        // That is OK for now: the only locals used in module evaluation
        // are comprehension bindings.
        let local_count = local_names.len().try_into().unwrap();
        alloca_frame(
            self.eval,
            local_count,
            bc.max_stack_size,
            bc.max_loop_depth,
            |eval| eval.eval_bc(const_frozen_string!("module").to_value(), &bc),
        )
    }

    fn eval_top_level_stmt(
        &mut self,
        stmt: &mut CstStmt,
        local_names: FrozenRef<'static, [FrozenStringValue]>,
    ) -> Result<Value<'v>, EvalException> {
        let stmts: &mut [CstStmt] = match &mut stmt.node {
            StmtP::Statements(stmts) => stmts,
            _ => slice::from_mut(stmt),
        };

        if stmts.len() != self.top_level_stmt_count {
            return Err(EvalException::new(
                ModuleError::TopLevelStmtCountMismatch.into(),
                stmt.span,
                &self.codemap,
            ));
        }

        if self.last_stmt_defining_type.is_none() {
            self.typecheck(stmts)?;
        }

        let mut last = Value::new_none();
        for i in 0..stmts.len() {
            self.populate_types_in_stmts(stmts, TopLevelStmtIndex(i + 1))?;

            let stmt = &mut stmts[i];

            match &mut stmt.node {
                StmtP::Load(load) => {
                    self.eval_load(Spanned {
                        node: load,
                        span: stmt.span,
                    })?;
                    last = Value::new_none();
                }
                _ => last = self.eval_regular_top_level_stmt(stmt, local_names)?,
            }

            if Some(TopLevelStmtIndex(i)) == self.last_stmt_defining_type {
                self.typecheck(stmts)?;
            }
        }
        Ok(last)
    }

    fn typecheck(&mut self, stmts: &mut [CstStmt]) -> Result<(), EvalException> {
        if !self.eval.static_typechecking {
            return Ok(());
        }

        self.populate_types_in_stmts(stmts, TopLevelStmtIndex(stmts.len()))?;

        let BindingsCollect { bindings, .. } =
            BindingsCollect::collect(stmts, TypecheckMode::Compiler, &self.codemap)
                .map_err(InternalError::into_eval_exception)?;

        let (errors, ..) = solve_bindings(&OracleAny, &self.globals, bindings, &self.codemap);

        if let Some(error) = errors.into_iter().next() {
            return Err(error.into_eval_exception());
        }

        Ok(())
    }

    pub(crate) fn eval_module(
        &mut self,
        mut stmt: CstStmt,
        local_names: FrozenRef<'static, [FrozenStringValue]>,
    ) -> Result<Value<'v>, EvalException> {
        self.enter_scope(ScopeId::module());
        let value = self.eval_top_level_stmt(&mut stmt, local_names)?;
        self.exit_scope();
        assert!(self.locals.is_empty());
        Ok(value)
    }
}