%% -*- mode: erlang; indent-tabs-mode: nil -*-
%% Copyright (c) 2008-2020 Robert Virding
%%
%% 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.

%%% File    : lfe_comp.erl
%%% Author  : Robert Virding
%%% Purpose : Lisp Flavoured Erlang compiler (to core Erlang).

%% All the code in the file is treated as one sequence of forms until
%% after the macroexpansion pass when it is split into separate
%% modules. However up until after the lint pass all errors and
%% warnings are collected together in the errors and warnings
%% fields. After this the errors become more module specific and are
%% kept together with the compiled code, both for core and the
%% following erlang formats.

-module(lfe_comp).

-export([format_error/1]).
-export([file/1,file/2,forms/1,forms/2,default_options/0]).

%% -compile(export_all).

-import(lists, [member/2,keyfind/3,filter/2,foreach/2,all/2,any/2,
                map/2,flatmap/2,foldl/3,foldr/3,mapfoldl/3,mapfoldr/3]).

-include("lfe.hrl").
-include("lfe_comp.hrl").

%% Mightn't use all commands in do_passes yet.
-dialyzer({[no_match],do_passes/2}).

%% The main compiler state.

-record(comp, {base="",                         %Base name
               ldir=".",                        %Lisp file dir
               lfile="",                        %Lisp file
               odir=".",                        %Output directory
               opts=[],                         %User options
               ipath=[],                        %Include path
               cinfo=none,                      %Common compiler info
               module=[],                       %Module name
               code=[],                         %Code after last pass.
               return=[],                       %What is returned [Val] | []
               errors=[],
               warnings=[],
               extra=[]                         %Pass specific options, plist
              }).

%% default_options() -> Options.
%%  Return the default compiler options.

-define(DEFAULT_OPTS, [verbose,report]).

%% Errors.
format_error(write_file) -> "error writing file".

default_options() -> ?DEFAULT_OPTS.

%% file(Name) ->
%%      {ok,Mod,Warns} | {ok,Mod,Binary,Ws} | {error,Errors,Warns} | error.
%% file(Name, Options) ->
%%      {ok,Mod,Warns} | {ok,Mod,Binary,Ws} | {error,Errors,Warns} | error.
%%  Compile the LFE file Name.

file(Name) -> file(Name, default_options()).

file(Name, Opts) -> do_compile({file,Name}, Opts).

%% forms(Forms) -> {ok,Mod,Bin,Warnings} | {error,Errors,Warnings}.
%% forms(Forms, Options) -> {ok,Mod,Bin,Warnings} | {error,Errors,Warnings}.
%%  Compile the LFE forms Forms, always return a binary.

forms(Forms) -> forms(Forms, default_options()).

forms(Forms, Opts) -> do_compile({forms,Forms}, Opts).

do_compile(Input, Opts) ->
    Ifun = fun () ->
                   Ret = try
                             internal(Input, Opts)
                         catch
                             ?CATCH(error, Reason, St)
                                 {error,{Reason,St}}
                         end,
                   exit(Ret)
           end,
    {Pid,Ref} = spawn_monitor(Ifun),
    receive
        {'DOWN',Ref,_,Pid,Res} -> Res
    end.

%% internal(Input, Options) -> Result.

internal({file,Name}, Opts) -> do_file(Name, Opts);
internal({forms,Forms}, Opts) -> do_forms(Forms, Opts).

do_file(Name, Opts0) ->
    Opts1 = lfe_comp_opts(Opts0),
    St0 = #comp{opts=Opts1,code=[]},            %Code must be list!
    St1 = filenames(Name, ".lfe", St0),
    St2 = include_path(St1),
    case lfe_io:parse_file(St2#comp.lfile) of
        {ok,Fs} ->
            %% Do the actual compilation work.
            do_forms(St2#comp{code=Fs});
        {error,Error} -> do_error_return(St2#comp{errors=[Error]})
    end.

do_forms(Fs0, Opts0) ->
    Source = lprop(source, Opts0, ""),
    Opts1 = lfe_comp_opts(Opts0),
    St0 = #comp{opts=[binary|Opts1]},           %Implicit binary option
    St1 = filenames(Source, "", St0),
    St2 = include_path(St1),
    %% Tag forms with a "line number", just use their index.
    {Fs1,_} = mapfoldl(fun (F, N) -> {{F,N},N+1} end, 1, Fs0),
    do_forms(St2#comp{code=Fs1}).

%% filenames(File, Suffix, State) -> State.
%%  The default output dir is the current directory unless an
%%  explicit one has been given in the options.

filenames(File, Suffix, St) ->
    %% Test for explicit outdir.
    Odir = outdir(St#comp.opts, "."),
    Ldir = filename:dirname(File),
    Base = filename:basename(File, Suffix),
    Lfile = lfefile(Ldir, Base, Suffix),
    St#comp{base=Base,
            ldir=Ldir,
            lfile=Lfile,
            odir=Odir
           }.

lfefile(".", Base, Suffix) -> Base ++ Suffix;
lfefile(Dir, Base, Suffix) ->
    filename:join(Dir, Base ++ Suffix).

outdir(Opts, Def) -> lprop(outdir, Opts, Def).

%% lprop(Key, PropList, Default) -> Value.
%%  Find Key, Val from PropList else Default.

lprop(Key, [{Key,Val}|_], _) -> Val;                %Erlang way
lprop(Key, [[Key,Val]|_], _) -> Val;                %LFE way
lprop(Key, [_|List], Def) -> lprop(Key, List, Def);
lprop(_, [], Def) -> Def.

%% include_path(State) -> State.
%%  Set the include path, we permit {i,Dir} and [i,Dir].

include_path(#comp{ldir=Ldir,opts=Opts}=St) ->
    Ifun = fun ({i,I}, Is) -> [I|Is];           %Erlang way
               ([i,I], Is) -> [I|Is];           %LFE way
               (_, Is) -> Is
           end,
    %% Same ordering as in the erlang compiler.
    Is = [".",Ldir|foldr(Ifun, [], Opts)],       %Default entries
    St#comp{ipath=Is}.

%% compiler_info(State) -> CompInfo.

compiler_info(#comp{lfile=F,opts=Os,ipath=Is}) ->
    #cinfo{file=F,opts=Os,ipath=Is}.

%% lfe_comp_opts(Opts) -> Opts.
%%  Translate from LFE to erlang standard options for lfe compiler.

lfe_comp_opts(Opts) ->
    Fun = fun ('to-split') -> to_split;
              ('to-expmac') -> to_expmac;
              ('to-expand') -> to_expand;
              ('to-lint') -> to_lint;
              ('no-docs') -> no_docs;
              ('to-erlang') -> to_erlang;
              ('to-core0') -> to_core0;
              ('to-core') -> to_core;
              ('to-kernel') -> to_kernel;
              ('to-asm') -> to_asm;
              ('to-ast') -> to_ast;             %The output should be an AST
              ('debug-info') -> debug_info;
              ('no-export-macros') -> no_export_macros;
              ('warnings-as-errors') -> warnings_as_errors;
              ('report-warnings') -> report_warnings;
              ('report-errors') -> report_errors;
              ('debug-print') -> debug_print;
              (O) -> O
          end,
    map(Fun, Opts).

%% do_forms(State) ->
%%      {ok,Mod,[Core],[Warnings]} | {error,Errors,Warnings} | error.
%%  Run the actual LFE compiler passes.

do_forms(St0) ->
    %% Fill in the common compiler info.
    St1 = St0#comp{cinfo=compiler_info(St0)},
    Ps = passes(),
    case do_passes(Ps, St1) of
        {ok,St2} -> do_ok_return(St2);
        {error,St2} -> do_error_return(St2)
    end.

%% passes() -> [Pass].
%% do_passes(Passes, State) -> {ok,State} | {error,State}.
%%
%%  {when_flag,Flag,Cmd}    Do Cmd if Flag is or is not in the
%%  {unless_flag,Flag,Cmd}  option list.
%%
%%  {when_test,Test,Cmd}    Do Cmd if the Test function returns 'true'
%%  {unless_test,Test,Cmd}  or 'false'.
%%
%%  {do,Fun}                Call Fun and then continue.
%%
%%  {listing,PrintFun}      End compilation calling PrintFun to output
%%                          file.
%%
%%  done                    End compilation.
%%
%%  {done,PrintFun}         End compilation calling PrintFun to output
%%                          file, unless 'binary' is specified in which
%%                          current code will be returned.

passes() ->
    [
     %% Split input file into separate modules.
     {do,fun do_split_file/1},
     {when_flag,to_split,{done,fun split_pp/1}},
     %% Do per-module macro processing.
     {unless_flag,no_export_macros,{do,fun do_export_macros/1}},
     {when_flag,to_expmac,{done,fun expmac_pp/1}},
     %% Now we expand and trim remaining macros.
     {do,fun do_expand_macros/1},
     {when_flag,to_expand,{done,fun expand_pp/1}},
     {do,fun do_lfe_lint/1},
     {when_flag,to_lint,{done,fun lint_pp/1}},
     {unless_flag,no_docs,{do,fun do_get_docs/1}},
     {do,fun do_lfe_codegen/1},
     {when_flag,to_erlang,{done,fun erlang_pp/1}},
     {do,fun do_erl_comp/1},
     %% These options will have made erlang compiler return internal
     %% form after pass.
     {when_flag,to_core0,{done,fun erl_core_pp/1}},
     {when_flag,to_core,{done,fun erl_core_pp/1}},
     {when_flag,to_kernel,{done,fun erl_kernel_pp/1}},
     {when_flag,to_asm,{done,fun erl_asm_pp/1}},
     %% Stop at non-binary returns, either return or drop.
     {unless_test,fun is_binary_module/1,done},
     %% Now we just write the beam file unless warnings-as-errors is
     %% set and we have warnings.
     {when_test,fun is_werror/1,error},
     %% Write docs beam chunks.
     {do,fun add_chunks/1},
     {done,fun beam_write/1}                    %Should be last
    ].

do_passes([{when_flag,Flag,Cmd}|Ps], #comp{opts=Opts}=St) ->
    do_passes(?IF(member(Flag, Opts), [Cmd|Ps], Ps), St);
do_passes([{unless_flag,Flag,Cmd}|Ps], #comp{opts=Opts}=St) ->
    do_passes(?IF(member(Flag, Opts), Ps, [Cmd|Ps]), St);
do_passes([{when_test,Test,Cmd}|Ps], St) ->
    do_passes(?IF(Test(St), [Cmd|Ps], Ps), St);
do_passes([{unless_test,Test,Cmd}|Ps], St) ->
    do_passes(?IF(Test(St), Ps, [Cmd|Ps]), St);
do_passes([{do,Fun}|Ps], St0) ->
    case Fun(St0) of
        {ok,St1} -> do_passes(Ps, St1);
        {error,St1} -> {error,St1}
    end;
do_passes([{listing,PrintFun}|_], St) ->
    PrintFun(St);
do_passes([done|_], St) -> {ok,St};             %Just end now
do_passes([error|_], St) -> {error,St};
do_passes([{done,Fun}|_], St) ->
    %% Print unless binary, in which case end.
    do_passes([{unless_flag,binary,{listing,Fun}}], St);
do_passes([], St) -> {ok,St}.                   %Got to the end, everything ok!

%% do_split_file(State) -> {ok,State} | {error,State}.
%%  Split a file into separate modules.  Everything defined before the
%%  first module is available in every module, after that things are
%%  local to the module in which they are defined. We need to expand
%%  top-level macros in forms so we can safelt detect the start of
%%  each module (with define-module form).

do_split_file(#comp{lfile=Lfile,cinfo=Ci,code=Code0}=St) ->
    %% Add a FILE macro with the file name at the beginning.
    Mac = {[defmacro,'FILE',[],?BQ(?Q(Lfile))],1},
    Code1 = [Mac|Code0],
    case collect_pre_forms(Code1, Ci) of        %Expand pre module forms
        {Pfs,Fs,Env0,Mst0} ->
            %% Expand the modules using the pre forms and environment.
            case collect_modules(Fs, Pfs, Env0, Mst0) of
                {ok,Ms,_Mst1} ->
                    {ok,St#comp{code=Ms,
                                warnings=St#comp.warnings}};
                {error,Es,Ws} ->
                    {error,St#comp{code=[],     %Pseudo module list.
                                   errors=St#comp.errors ++ Es,
                                   warnings=St#comp.warnings ++ Ws}}
            end;
        {error,Es,Ws} ->
            {error,St#comp{code=[],             %Pseudo module list.
                           errors=St#comp.errors ++ Es,
                           warnings=St#comp.warnings ++ Ws}}
    end.

%% collect_pre_forms(Forms, CompInfo) ->
%%     {PreForms,RestForms,Env,State}.

collect_pre_forms(Fs, Ci) ->
    Env = lfe_env:new(),
    %% Don't deep expand, keep everything.
    Mst = lfe_macro:expand_form_init(Ci, false, true),
    collect_mod_forms(Fs, Env, Mst).

%% collect_modules(Forms, PreForms, PreEnv, MacroState) ->
%%     {Modules,MacroState}.
%%  Collect and expand modules upto the end. Each module initially has
%%  the pre environment and all pre forms are appended to it.

collect_modules(Fs, PreFs, PreEnv, Mst) ->
    collect_modules(Fs, [], PreFs, PreEnv, Mst).

collect_modules([{['define-module',Name|_],_}=Mdef|Fs0], Ms, PreFs, PreEnv, Mst0) ->
    %% Expand and collect all forms upto next define-module or end.
    case collect_mod_forms(Fs0, PreEnv, Mst0) of
        {Mfs0,Fs1,_,Mst1} ->
            M = #module{name=Name,code=[Mdef] ++ PreFs ++ Mfs0},
            collect_modules(Fs1, [M|Ms], PreFs, PreEnv, Mst1);
        Error -> Error
    end;
collect_modules([], Ms, _PreFs, _PreEnv, Mst) ->
    {ok,lists:reverse(Ms),Mst}.

%% collect_mod_forms(Forms, Env, MacroState) ->
%% collect_mod_forms(Forms, Acc, Env, MacroState) ->
%%     {Modforms,RestForms,Env,MacroState}.
%%  Expand and collect forms upto the next define-module or end. We
%%  also flatten top-level nested progn code.

collect_mod_forms(Fs, Env0, Mst0) ->
    case collect_mod_forms(Fs, [], Env0, Mst0) of
        {Acc,Rest,Env1,Mst1} ->
            {lists:reverse(Acc),Rest,Env1,Mst1};
        {error,_,_}=Error -> Error
    end.

collect_mod_forms([F0|Fs0], Acc, Env0, Mst0) ->
    case lfe_macro:expand_fileform(F0, Env0, Mst0) of
        {ok,{['define-module'|_],_}=F1,Env1,Mst1} ->
            {Acc,[F1|Fs0],Env1,Mst1};
        {ok,{['progn'|Pfs],L},Env1,Mst1} ->     %Flatten progn's
            Fs1 = [ {F,L} || F <- Pfs ] ++ Fs0,
            collect_mod_forms(Fs1, Acc, Env1, Mst1);
        {ok,F1,Env1,Mst1} ->
            collect_mod_forms(Fs0, [F1|Acc], Env1, Mst1);
        {error,Es,Ws,_} -> {error,Es,Ws}
    end;
collect_mod_forms([], Acc, Env, Mst) -> {Acc,[],Env,Mst}.

%% do_export_macros(State) -> {ok,State} | {error,State}.
%% do_expand_macros(State) -> {ok,State} | {error,State}.
%%  Process the macros in each module. Do_expand_macros is the last
%%  pass which fully expands all remaining macros and flattens the
%%  output.

do_export_macros(#comp{cinfo=Ci,code=Ms0}=St) ->
    Umac = fun (#module{code=Mfs0}=Mod) ->
                   {Mfs1,_} = lfe_macro_export:module(Mfs0, Ci),
                   Mod#module{code=Mfs1}
           end,
    Ms1 = lists:map(Umac, Ms0),
    {ok,St#comp{code=Ms1}}.

do_expand_macros(#comp{cinfo=Ci,code=Ms0}=St0) ->
    Emac = fun (#module{code=Fs0}=Mod) ->
                   Env = lfe_env:new(),
                   %% Deep expand, keep everything.
                   Mst = lfe_macro:expand_form_init(Ci, true, true),
                   case process_forms(fun expand_form/3, Fs0, {Env,Mst}) of
                       {Fs1,_} -> Mod#module{code=Fs1};
                       {error,_,_}=Error -> Error
                   end
           end,
    Ms1 = lists:map(Emac, Ms0),
    St1 = St0#comp{code=Ms1},
    ?IF(all_module(Ms1), {ok,St1}, {error,St1}).

expand_form(F0, L, {Env0,St0}) ->
    case lfe_macro:expand_form(F0, L, Env0, St0) of
        {ok,[progn|Pfs],Env1,St1} ->
            process_forms(fun expand_form/3, Pfs, L, {Env1,St1});
        %%{ok,['eval-when-compile'|_],Env1,St1} ->
        %%    {[],{Env1,St1}};
        {ok,F1,Env1,St1} ->
            {[{F1,L}],{Env1,St1}};
        {error,Es,Ws,_} -> throw({expand_form,{error,Es,Ws}})
    end.

%% process_forms(Fun, Forms, State) -> {Forms,State} | Error.
%% process_forms(Fun, Forms, Line, State) -> {Forms,State} | Error.
%%  Wrappers around lfe_lib:proc_forms which catch thrown errors.

process_forms(Fun, Fs, St) ->
    try lfe_lib:proc_forms(Fun, Fs, St)
    catch
        throw:{expand_form,Error} -> Error
    end.

process_forms(Fun, Fs, L, St) ->
    try lfe_lib:proc_forms(Fun, Fs, L, St)
    catch
        throw:{expand_form,Error} -> Error
    end.

%% do_lint(State) -> {ok,State} | {error,State}.
%% do_get_docs(State) -> {ok,State} | {error,State}.
%% do_lfe_codegen(State) -> {ok,State} | {error,State}.
%% do_erl_comp(State) -> {ok,State} | {error,State}.
%%  The actual compiler passes.

do_lfe_lint(#comp{cinfo=Ci,code=Ms0}=St0) ->
    Lint = fun (#module{code=Mfs,warnings=Ws}=Mod) ->
                   case lfe_lint:module(Mfs, Ci) of
                       {ok,Name,Lws} -> Mod#module{name=Name,warnings=Ws++Lws};
                       {error,Les,Lws} -> {error,Les,Ws++Lws}
                   end
           end,
    %% Lint the modules, then check if all are ok.
    Ms1 = lists:map(Lint, Ms0),
    St1 = St0#comp{code=Ms1},
    ?IF(all_module(Ms1), {ok,St1}, {error,St1}).

do_get_docs(#comp{code=Ms0,opts=Opts}=St) ->
    Doc = fun (#module{code=Mfs,chunks=Chks}=Mod) ->
                  {ok,Chunk} = lfe_docs:make_chunk(Mfs, Opts),
                  Mod#module{chunks=[Chunk|Chks]}
          end,
    Ms1 = lists:map(Doc, Ms0),
    {ok,St#comp{code=Ms1}}.

do_lfe_codegen(#comp{cinfo=Ci,code=Ms0}=St0) ->
    Code = fun (#module{name=Name,code=Mfs,warnings=Ws}=Mod) ->
                   case lfe_codegen:module(Mfs, Ci) of
                       {ok,Name,AST,Gws} ->    %Name consistency check!
                           Mod#module{code=AST,warnings=Ws ++ Gws};
                       {error,Ges,Gws} ->
                           {error,Ges,Gws}
                   end
           end,
    Ms1 = lists:map(Code, Ms0),
    St1 = St0#comp{code=Ms1},
    ?IF(all_module(Ms1), {ok,St1}, {error,St1}).

do_erl_comp(#comp{code=Ms0}=St0) ->
    ErlOpts = erl_comp_opts(St0),               %Options to erlang compiler
    %% Compile all the modules, then if all are ok.
    Ms1 = lists:map(fun (M) -> do_erl_comp_mod(M, ErlOpts) end, Ms0),
    St1 = St0#comp{code=Ms1},
    ?IF(all_module(Ms1), {ok,St1}, {error,St1}).

do_erl_comp_mod(#module{code=Core,warnings=Ws}=Mod, ErlOpts) ->
    %% lfe_io:format("~p\n", [Core]),
    case compile:forms(Core, ErlOpts) of
        {ok,_,Result,Ews} ->
            Mod#module{code=Result,warnings=Ws ++ fix_erl_errors(Ews)};
        {error,Ees,Ews} ->
            {error,fix_erl_errors(Ees),fix_erl_errors(Ews)}
    end.

all_module(Res) -> lists:all(fun (X) -> is_record(X, module) end, Res).

%% erl_comp_opts(State) -> Options.
%%  Strip out report options and make sure erlang compiler returns
%%  errors and warnings. Also remove other options which might cause
%%  strange behaviour.

erl_comp_opts(St) ->
    Os0 = St#comp.opts,
    Filter = fun (report) -> false;             %No reporting!
                 (report_warnings) -> false;
                 (report_errors) -> false;
                 (warnings_as_errors) -> false; %We handle these ourselves
                 ({source,_}) -> false;
                 (_) -> true                    %Everything else
             end,
    Os1 = filter(Filter, Os0),
    %% Now build options for the erlang compiler. 'no_bopt' turns off
    %% an optimisation in the guard which crashes our code.
    [{source,St#comp.lfile},                    %Set the source file
     return,                                    %Ensure we return something
     binary,                                    %We want a binary
     nowarn_unused_vars|                        %Don't need to know here
     Os1].

%% split_pp(State) -> {ok,State} | {error,State}.
%% expmac_pp(State) -> {ok,State} | {error,State}.
%% expand_pp(State) -> {ok,State} | {error,State}.
%% lint_pp(State) -> {ok,State} | {error,State}.
%% sexpr_pp(State) -> {ok,State} | {error,State}.
%% erl_core_pp(State) -> {ok,State} | {error,State}.
%% erl_kernel_pp(State) -> {ok,State} | {error,State}.
%% erl_asm_pp(State) -> {ok,State} | {error,State}.
%% beam_write(State) -> {ok,State} | {error,State}.
%%  Output the various file types. The XXX_pp functions output with
%%  the same name as the input file while beam_write outputs to the
%%  module name.

%% This just print the whole file structure.
split_pp(St) -> sexpr_pp(St, "split").
expmac_pp(St) -> sexpr_pp(St, "expmac").
expand_pp(St) -> sexpr_pp(St, "expand").
lint_pp(St) -> sexpr_pp(St, "lint").

sexpr_pp(St, Ext) ->
    Save = fun (File, #module{code=Code}) ->
                   lfe_io:prettyprint(File, Code), io:nl(File)
           end,
    do_list_save_file(Save, Ext, St).

%% These print a list of module structures.
erlang_pp(#comp{opts=Opts}=St) ->
    Format = ?IF(member(to_ast, Opts),
                 fun (F) -> io_lib:format("~p.\n", [F]) end,
                 fun (F) -> [erl_pp:form(F),$\n] end),
    Save = fun (File, #module{code=AST}) ->
                   Chars = [ Format(F) || F <- AST ],
                   io:put_chars(File, Chars)
           end,
    do_list_save_file(Save, "erl", St).

erl_core_pp(#comp{opts=Opts}=St) ->
    Format = ?IF(member(to_ast, Opts),
                 fun (F) -> io_lib:format("~p.\n", [F]) end,
                 fun (F) -> [core_pp:format(F),$\n] end),
    Save = fun (File, #module{code=Core}) ->
                   io:put_chars(File, Format(Core))
           end,
    do_list_save_file(Save, "core", St).

erl_kernel_pp(#comp{opts=Opts}=St) ->
    Format = ?IF(member(to_ast, Opts),
                 fun (F) -> io_lib:format("~p.\n", [F]) end,
                 fun (F) -> [v3_kernel_pp:format(F),$\n] end),
    Save = fun (File, #module{code=Kern}) ->
                   io:put_chars(File, Format(Kern))
           end,
    do_list_save_file(Save, "kernel", St).

erl_asm_pp(St) ->
    Save = fun (File, #module{code=Asm}) ->
                   beam_listing:module(File, Asm), io:nl(File)
           end,
    do_list_save_file(Save, "S", St).

do_list_save_file(SaveOne, Ext, St) ->
    SaveAll = fun (File, Code) ->

                      lists:foreach(fun (C) -> SaveOne(File, C) end, Code)
              end,
    do_save_file(SaveAll, Ext, St).

do_save_file(SaveAll, Ext, St) ->
    Name = filename:join(St#comp.odir, St#comp.base ++ ["."|Ext]),
    %% delayed_write useful here but plays havoc with erjang.
    case file:open(Name, [write]) of
        {ok,File} ->
            Ret = SaveAll(File, St#comp.code),
	    ok = file:close(File),
            case Ret of
                ok -> {ok,St};
                {error,_} ->
                    %% Just signal we couldn't write the file.
                    {error,St#comp{errors=[{lfe_comp,write_file}]}}
            end;
        {error,_} ->
            %% Just signal we couldn't write the file.
            {error,St#comp{errors=[{lfe_comp,write_file}]}}
    end.

add_chunks(#comp{code=Ms0}=St) ->
    Add = fun (#module{name=Name,code=Beam0,chunks=Chks}=Mod) ->
                  if Chks =:= [] -> Mod;        %Nothing to do
                     true ->
                          {ok,Name,All} = beam_lib:all_chunks(Beam0),
                          {ok,Beam1} = beam_lib:build_module(Chks ++ All),
                          Mod#module{code=Beam1}
                  end
          end,
    Ms1 = lists:map(Add, Ms0),
    {ok,St#comp{code=Ms1}}.

beam_write(St0) ->
    Ms1 = lists:map(fun (M) -> beam_write_module(M, St0) end, St0#comp.code),
    St1 = St0#comp{code=Ms1},
    %% Check return status.
    ?IF(all_module(Ms1), {ok,St1}, {error,St1}).

beam_write_module(#module{name=M,code=Beam}=Mod, St) ->
    Name = filename:join(St#comp.odir, lists:concat([M,".beam"])),
    case file:write_file(Name, Beam) of
        ok -> Mod;
        {error,_} ->
            %% Just signal we couldn't write the file.
            {error,[{lfe_comp,write_file}],[]}
    end.

%% fix_erl_errors([{File,Errors}]) -> Errors.

fix_erl_errors(Fes) -> flatmap(fun ({_,Es}) -> Es end, Fes).

%% is_binary_module(State) -> true | false.
%%  Check whether the module code is a binary or not.

is_binary_module(#comp{code=Mods}) ->
    case Mods of
        [#module{code=Code}|_] when is_binary(Code) -> true;
        _ -> false
        %% _ -> io:format("ibr: ~p\n", [Mods]), false
    end.

%% is_werror(State) -> true | false.
%%  Check if warnings_as_errors is set and we have warnings.

is_werror(#comp{code=Code,opts=Opts,warnings=Ws}) ->
    case member(warnings_as_errors, Opts) of
        true ->
            (Ws =/= []) orelse
                any(fun (#module{warnings=Mws}) -> Mws =/= [] end, Code);
        false -> false
    end.

%% do_ok_return(State) -> {ok,Mod,...}.
%% do_error_return(State) -> {error,...} | error.
%%  Note that this handling of 'warnings_as_errors' differs from the
%%  vanilla erlang compiler 'compile'. We explicitly check for it.

do_ok_return(#comp{code=Code,lfile=Lfile,opts=Opts,warnings=Ws}) ->
    ?WHEN_OPT(report, Opts, fun () -> list_warnings(Lfile, Ws) end),
    %% Fix right return.
    Report = member(report, Opts),
    Return = member(return, Opts),
    Binary = member(binary, Opts),
    RetMod = fun (M) -> ok_return_mod(M, Report, Return, Binary, Lfile) end,
    Ret0 = lists:map(RetMod, Code),
    Ret1 = if Return -> [Ret0,return_ews(Lfile, Ws)];
              true -> [Ret0]
           end,
    list_to_tuple([ok|Ret1]).                   %And build the ok tuple

ok_return_mod(#module{name=Name,code=Mods,warnings=Ws}, Report, Return, Binary, Lfile) ->
    Report andalso list_warnings(Lfile, Ws),
    Ret0 = if Return -> [return_ews(Lfile, Ws)];
              true -> []
           end,
    Ret1 = if Binary -> [Mods|Ret0];
              true -> Ret0
           end,
    list_to_tuple([ok,Name|Ret1]).              %And build the ok tuple

do_error_return(#comp{code=Code,lfile=Lfile,opts=Opts,errors=Es,warnings=Ws}) ->
    ?WHEN_OPT(report, Opts, fun () -> list_errors(Lfile, Es) end),
    ?WHEN_OPT(report, Opts, fun () -> list_warnings(Lfile, Ws) end),
    Report = lists:member(report, Opts),
    Return = lists:member(return, Opts),
    RetMod = fun (M) -> error_return_mod(M, Report, Return, Lfile) end,
    Err = lists:map(RetMod, Code),
    %% Fix right return.
    ?IF(Return, {error,Err,return_ews(Lfile, Es),return_ews(Lfile, Ws)}, error).

error_return_mod(#module{warnings=Ws}, Rep, _, Lfile) ->
    Rep andalso list_warnings(Lfile, Ws),
    {error,[],return_ews(Lfile, Ws)};           %No errors, only warnings
error_return_mod({error,Es,Ws}, Rep, _, Lfile) ->
    Rep andalso list_errors(Lfile, Es),
    Rep andalso list_warnings(Lfile, Ws),
    {error,return_ews(Lfile, Es),return_ews(Lfile, Ws)}.

return_ews(_, []) -> [];
return_ews(Lfile, Es) -> [{Lfile,Es}].

list_warnings(F, Ws) ->
    foreach(fun ({Line,Mod,Warn}) ->
                    Cs = Mod:format_error(Warn),
                    lfe_io:format("~s:~w: Warning: ~s\n", [F,Line,Cs]);
                ({Mod,Warn}) ->
                    Cs = Mod:format_error(Warn),
                    lfe_io:format("~s: Warning: ~s\n", [F,Cs])
            end, Ws).

list_errors(F, Es) ->
    foreach(fun ({Line,Mod,Error}) ->
                    Cs = Mod:format_error(Error),
                    lfe_io:format("~s:~w: ~s\n", [F,Line,Cs]);
                ({Mod,Error}) ->
                    Cs = Mod:format_error(Error),
                    lfe_io:format("~s: ~s\n", [F,Cs])
            end, Es).
