%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 1999-2026. All Rights Reserved.
%%
%% 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
%%
%%     http://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.
%%
%% %CopyrightEnd%
%%
%% Purpose : Replace values by aliases from patterns optimisation for Core

%% Replace expressions by aliases from patterns. For example:
%%
%%    example({ok, Val}) ->
%%        {ok, Val}.
%%
%% will become:
%%
%%    example({ok, Val} = Tuple) ->
%%        Tuple.
%%
%% Currently this pass aliases tuple and cons nodes made of literals,
%% variables and other cons. The tuple/cons may appear anywhere in the
%% pattern and it will be aliased if used later on.
%%
%% Notice a tuple/cons made only of literals is not aliased as it may
%% be part of the literal pool.

-module(sys_core_alias).
-moduledoc false.

-compile([{nowarn_possibly_unsafe_function, {erlang, list_to_atom, 1}}]).

-export([module/2]).

-include("core_parse.hrl").

-define(NOTSET, 0).
-define(HAS_SUBS(Sub), Sub#sub.t =/= none).

-record(sub, {p=#{} :: #{term() => ?NOTSET | atom()},              %% Found pattern substitutions
              v=sets:new() :: sets:set(cerl:var_name()), %% Variables used by patterns
              t=none :: temp()}).                                  %% Temporary information from pre to post

-type sub() :: #sub{}.

%% temp keeps the stack of substitutions.
%%
%% 1. none is the root
%% 2. temp stores the previous temp value - this is used when
%%    we update the existing sub instead of replacing it
%% 3. sub is the previous substitution - this is used when
%%    we replace the current sub
%% 4. clause is used when there are clauses with patterns
%%
-type temp() ::
        none
        | {temp, temp()}
        | {sub, sub()}
        | {clause, [term()], [term()], sets:set(cerl:var_name()), temp()}.

-spec module(cerl:c_module(), [compile:option()]) ->
        {'ok',cerl:c_module(),[]}.

module(#c_module{defs=Ds0}=Mod, _Opts) ->
    Ds1 = [def(D) || D <- Ds0],
    {ok,Mod#c_module{defs=Ds1},[]}.

def({#c_var{name={F,Arity}}=Name,B0}) ->
    try
        put(new_var_num, 0),
        {B1,_} = cerl_trees:mapfold(fun pre/2, fun post/2, sub_new(none), B0),
        erase(new_var_num),
        {Name,B1}
    catch
        Class:Error:Stack ->
            io:fwrite("Function: ~w/~w\n", [F,Arity]),
            erlang:raise(Class, Error, Stack)
    end.

pre(#c_clause{pats=Pats}=Node, Sub0) ->
    case get_pattern_keys(Pats) of
        [] when ?HAS_SUBS(Sub0) ->
            VarNames = get_variables(Pats),
            {Node,sub_fold(VarNames, Sub0)};
        [] ->
            {Node,Sub0};
        Keys ->
            VarNames = get_variables(Pats),
            Sub1 = sub_fold(VarNames, Sub0),
            Sub2 = sub_add_keys(Keys, Sub1),
            #sub{v=SubNames,t=Temp} = Sub2,
            Sub3 = Sub2#sub{v=merge_variables(VarNames, SubNames),
                            t={clause,Pats,Keys,SubNames,Temp}},
            {Node#c_clause{pats=[]},Sub3}
    end;

pre(#c_let{vars=Vars}=Node, Sub) when ?HAS_SUBS(Sub) ->
    {Node,sub_fold(get_variables(Vars), Sub)};

pre(#c_fun{vars=Vars}=Node, Sub) when ?HAS_SUBS(Sub) ->
    {Node,sub_fold(get_variables(Vars), Sub)};

pre(Node, Sub0) when ?HAS_SUBS(Sub0) ->
    case cerl:is_literal(Node) of
        true ->
            skip;
        false ->
            %% We cache only tuples and cons.
            case cerl:is_data(Node) of
                false ->
                    {Node,Sub0};
                true ->
                    Kind = cerl:data_type(Node),
                    Es = cerl:data_es(Node),
                    case sub_cache_nodes(Kind, Es, Sub0) of
                        {Name,Sub1} ->
                            {cerl:ann_c_var(cerl:get_ann(Node), Name),Sub1};
                        error ->
                            {Node,Sub0}
                    end
            end
    end;

pre(Node, Sub) -> {Node, Sub}.

post(#c_clause{}=Node, #sub{t={clause,Pats0,Keys,V,T}}=Sub0) ->
    {Sub1,PostKeys} = sub_take_keys(Keys, Sub0),
    Pats1 = put_pattern_keys(Pats0, PostKeys),
    Sub2 = sub_unfold(Sub1#sub{v=V,t=T}),
    {Node#c_clause{pats=Pats1},Sub2};

post(#c_clause{}=Node, Sub) ->
    {Node,sub_unfold(Sub)};

post(#c_let{}=Node, Sub) ->
    {Node,sub_unfold(Sub)};

post(#c_fun{}=Node, Sub) ->
    {Node,sub_unfold(Sub)};

post(Node, Sub) ->
    {Node,Sub}.

%% sub_new/1
%% sub_add_keys/2
%% sub_take_keys/3
%% sub_cache_nodes/3
%%
%% Manages the substitutions record.

%% Builds a new sub.
-spec sub_new(term()) -> sub().
sub_new(Temp) ->
    #sub{t=Temp}.

%% Folds the sub into a new one.
%% If the variables are disjoint, it means we can continue reusing
%% the patterns collected so far. Otherwise, it means a variable
%% was reassigned, so we build a new sub with fresh patterns.
sub_fold(VarNames, #sub{v=SubNames}=Sub) ->
    case is_disjoint_variables(VarNames, SubNames) of
        true  -> Sub#sub{t={temp,Sub#sub.t}};
        false -> sub_new({sub,Sub})
    end.

%% Unfolds the sub in case one was folded in the previous step,
%% otherwise returns the (root) Sub itself.
sub_unfold(#sub{t=none}=Sub) ->
    Sub;
sub_unfold(#sub{t={temp,Temp}}=Sub) ->
    Sub#sub{t=Temp};
sub_unfold(#sub{t={sub,Sub}}) ->
    Sub.

%% Adds the keys extracted from patterns to the state.
-spec sub_add_keys([term()], sub()) -> sub().
sub_add_keys(Keys, #sub{p=Pat0}=Sub) ->
    Pat1 =
        lists:foldl(fun(Key, Acc) ->
            false = maps:is_key(Key, Acc),         %Assertion.
            maps:put(Key, ?NOTSET, Acc)
        end, Pat0, Keys),
    Sub#sub{p=Pat1}.

%% Take the keys from the map taking into account the keys
%% that have changed as those must become aliases in the pattern.
-spec sub_take_keys([term()], sub()) -> {sub(), [{term(), atom()}]}.
sub_take_keys(Keys, #sub{p=Pat0}=Sub) ->
    {Pat1,Acc} = sub_take_keys(Keys, Pat0, []),
    {Sub#sub{p=Pat1},Acc}.

sub_take_keys([K|T], Sub0, Acc) ->
    case maps:take(K, Sub0) of
        {?NOTSET,Sub1} ->
            sub_take_keys(T, Sub1, Acc);
        {Name,Sub1} ->
            sub_take_keys(T, Sub1, [{K,Name}|Acc])
    end;
sub_take_keys([], Sub, Acc) ->
    {Sub,Acc}.

%% Check if the node can be cached based on the state information.
%% If it can be cached and it does not have an alias for it, we
%% build one.
-spec sub_cache_nodes(atom(), [cerl:cerl()], sub()) -> {atom(), sub()} | error.
sub_cache_nodes(Kind, Nodes, #sub{p=Pat}=Sub) ->
    case nodes_to_key(Kind, Nodes) of
        {ok, Key} ->
            case Pat of
                #{Key := ?NOTSET} ->
                    new_var_name(Key, Sub);
                #{Key := Name} ->
                    {Name,Sub};
                #{} ->
                    error
            end;
        error ->
            error
    end.

new_var_name(Key, #sub{p=Pat}=Sub) ->
    Counter = get(new_var_num),
    Name = list_to_atom("@r" ++ integer_to_list(Counter)),
    put(new_var_num, Counter + 1),
    {Name,Sub#sub{p=maps:put(Key, Name, Pat)}}.

%% get_variables/1
%% is_disjoint_variables/2
%% merge_variables/2

get_variables(NodesList) ->
    sets:from_list([Var || Node <- NodesList, Var <- cerl_trees:variables(Node)]).

is_disjoint_variables(Vars1, Vars2) ->
    sets:is_disjoint(Vars1, Vars2).

merge_variables(Vars1, Vars2) ->
    sets:union(Vars1, Vars2).

%% get_pattern_keys/2
%% put_pattern_keys/2
%%
%% Gets keys from patterns or add them as aliases.

get_pattern_keys(Patterns) ->
    lists:foldl(fun get_pattern_keys/2, [], Patterns).

get_pattern_keys(#c_tuple{es=Es}, Acc0) ->
    Acc1 = accumulate_pattern_keys(tuple, Es, Acc0),
    lists:foldl(fun get_pattern_keys/2, Acc1, Es);
get_pattern_keys(#c_cons{hd=Hd,tl=Tl}, Acc0) ->
    Acc1 = accumulate_pattern_keys(cons, [Hd, Tl], Acc0),
    get_pattern_keys(Tl, get_pattern_keys(Hd, Acc1));
get_pattern_keys(#c_alias{pat=Pat}, Acc0) ->
    get_pattern_keys(Pat, Acc0);
get_pattern_keys(#c_map{es=Es}, Acc0) ->
    lists:foldl(fun get_pattern_keys/2, Acc0, Es);
get_pattern_keys(#c_map_pair{val=Val}, Acc0) ->
    get_pattern_keys(Val, Acc0);
get_pattern_keys(_, Acc) ->
    Acc.

accumulate_pattern_keys(Kind, Nodes, Acc) ->
    case nodes_to_key(Kind, Nodes) of
        {ok,Key} -> [Key|Acc];
        error -> Acc
    end.

put_pattern_keys(Patterns, []) ->
    Patterns;
put_pattern_keys(Patterns, Keys) ->
    {NewPatterns,Map} =
        lists:mapfoldl(fun alias_pattern_keys/2, maps:from_list(Keys), Patterns),
    %% Check all aliases have been consumed from the map.
    0 = map_size(Map),
    NewPatterns.

alias_pattern_keys(#c_tuple{anno=Anno,es=Es0}=Node, Acc0) ->
    {Es1,Acc1} = lists:mapfoldl(fun alias_pattern_keys/2, Acc0, Es0),
    nodes_to_alias(tuple, Es0, Anno, Node#c_tuple{es=Es1}, Acc1);
alias_pattern_keys(#c_cons{anno=Anno,hd=Hd0,tl=Tl0}=Node, Acc0) ->
    {Hd1,Acc1} = alias_pattern_keys(Hd0, Acc0),
    {Tl1,Acc2} = alias_pattern_keys(Tl0, Acc1),
    nodes_to_alias(cons, [Hd0, Tl0], Anno, Node#c_cons{hd=Hd1,tl=Tl1}, Acc2);
alias_pattern_keys(#c_alias{pat=Pat0}=Node, Acc0) ->
    {Pat1,Acc1} = alias_pattern_keys(Pat0, Acc0),
    {Node#c_alias{pat=Pat1}, Acc1};
alias_pattern_keys(#c_map{es=Es0}=Node, Acc0) ->
    {Es1,Acc1} = lists:mapfoldl(fun alias_pattern_keys/2, Acc0, Es0),
    {Node#c_map{es=Es1}, Acc1};
alias_pattern_keys(#c_map_pair{val=Val0}=Node, Acc0) ->
    {Val1,Acc1} = alias_pattern_keys(Val0, Acc0),
    {Node#c_map_pair{val=Val1}, Acc1};
alias_pattern_keys(Pattern, Acc) ->
    {Pattern,Acc}.

%% Check if a node must become an alias because
%% its pattern was used later on as an expression.
nodes_to_alias(Kind, Inner, Anno, Node, Keys0) ->
    case nodes_to_key(Kind, Inner) of
        {ok,Key} ->
            case maps:take(Key, Keys0) of
                {Name,Keys1} ->
                    Var = cerl:ann_c_var(Anno, Name),
                    {cerl:ann_c_alias(Anno, Var, Node), Keys1};
                error ->
                    {Node,Keys0}
            end;
        error ->
            {Node,Keys0}
     end.

%% Builds the key used to check if a value can be
%% replaced by an alias. It considers literals,
%% aliases, variables, tuples and cons recursively.
%%
%% We bail out after an arbitrary amount of nodes
%% as very complex expressions may take forever to
%% process otherwise.
-define(DEPTH_LIMIT, 100).
nodes_to_key(Kind, Nodes) ->
    ntk_1(Nodes, [], Kind, ?DEPTH_LIMIT).

ntk_1(_, _Acc, _Kind, 0) ->
    error;
ntk_1([#c_alias{var=Var}|T], Acc, Kind, N) ->
    ntk_1([Var|T], Acc, Kind, N - 1);
ntk_1([#c_var{name=Name}|T], Acc, Kind, N) ->
    ntk_1(T, [[var,Name]|Acc], Kind, N - 1);
ntk_1([Node|T], Acc0, Kind, N) ->
    case cerl:is_data(Node) of
        true ->
            case ntk_1(cerl:data_es(Node), [], cerl:data_type(Node), N - 1) of
                {ok,Key} ->
                    ntk_1(T, [Key|Acc0], Kind, N - 1);
                error ->
                    error
            end;
        false ->
            error
    end;
ntk_1([], Acc, Kind, _N) ->
    {ok,[Kind|Acc]}.
