(***********************************************************************)
(*                                                                     *)
(*                           Objective Caml                            *)
(*                                                                     *)
(*            Xavier Leroy, projet Cristal, INRIA Rocquencourt         *)
(*                                                                     *)
(*  Copyright 2002 Institut National de Recherche en Informatique et   *)
(*  en Automatique.  All rights reserved.  This file is distributed    *)
(*  under the terms of the Q Public License version 1.0.               *)
(*                                                                     *)
(***********************************************************************)

(* $Id: bytepackager.ml,v 1.1 2002/02/08 16:55:42 xleroy Exp $ *)

(* "Package" a set of .cmo files into one .cmo file having the
   original compilation units as sub-modules. *)

open Misc
open Instruct
open Opcodes
open Emitcode

type error =
    Forward_reference of string * Ident.t
  | Multiple_definition of string * Ident.t
  | Not_an_object_file of string
  | Illegal_renaming of string * string
  | File_not_found of string

exception Error of error

(* References accumulating informations on the .cmo files *)

let relocs = ref ([] : (reloc_info * int) list)
let events = ref ([] : debug_event list)
let primitives = ref ([] : string list)
let force_link = ref false

(* Record a relocation.  Update its offset, and rename GETGLOBAL and
   SETGLOBAL relocations that correspond to one of the units being
   consolidated. *)

let rename_relocation objfile mapping defined base (rel, ofs) =
  let rel' =
    match rel with
      Reloc_getglobal id ->
        begin try
          let id' = List.assoc id mapping in
          if List.mem id defined
          then Reloc_getglobal id'
          else raise(Error(Forward_reference(objfile, id)))
        with Not_found ->
          rel
        end
    | Reloc_setglobal id ->
        begin try
          let id' = List.assoc id mapping in
          if List.mem id defined
          then raise(Error(Multiple_definition(objfile, id)))
          else Reloc_setglobal id'
        with Not_found ->
          rel
        end
    | _ ->
        rel in
  relocs := (rel', base + ofs) :: !relocs

(* Record and relocate a debugging event *)

let relocate_debug base ev =
  ev.ev_pos <- base + ev.ev_pos;
  events := ev :: !events

(* Read the unit information from a .cmo file. *)

let read_unit_info objfile =
  let ic = open_in_bin objfile in
  try
    let buffer = String.create (String.length Config.cmo_magic_number) in
    really_input ic buffer 0 (String.length Config.cmo_magic_number);
    if buffer <> Config.cmo_magic_number then
      raise(Error(Not_an_object_file objfile));
    let compunit_pos = input_binary_int ic in
    seek_in ic compunit_pos;
    let compunit = (input_value ic : compilation_unit) in
    if compunit.cu_name
    <> String.capitalize(Filename.basename(chop_extension_if_any objfile))
    then raise(Error(Illegal_renaming(objfile, compunit.cu_name)));
    close_in ic;
    compunit
  with x ->
    close_in ic;
    raise x

(* Read the bytecode from a .cmo file.
   Write bytecode to channel [oc].
   Rename globals as indicated by [mapping] in reloc info.
   Accumulate relocs, debug info, etc.
   Return size of bytecode. *)

let rename_append_bytecode oc mapping defined ofs (objfile, compunit) =
  let ic = open_in_bin objfile in
  try
    Bytelink.check_consistency objfile compunit;
    List.iter
      (rename_relocation objfile mapping defined ofs)
      compunit.cu_reloc;
    primitives := compunit.cu_primitives @ !primitives;
    if compunit.cu_force_link then force_link := true;
    seek_in ic compunit.cu_pos;
    Misc.copy_file_chunk ic oc compunit.cu_codesize;
    if !Clflags.debug && compunit.cu_debug > 0 then begin
      seek_in ic compunit.cu_debug;
      List.iter (relocate_debug ofs) (input_value ic);
    end;
    close_in ic;
    compunit.cu_codesize
  with x ->
    close_in ic;
    raise x

(* Same, for a list of .cmo files.  Return total size of bytecode. *)

let rec rename_append_bytecode_list oc mapping defined ofs = function
    [] ->
      ofs
  | ((objfile, compunit) as obj_unit) :: rem ->
      let size = rename_append_bytecode oc mapping defined ofs obj_unit in
      rename_append_bytecode_list
        oc mapping (Ident.create_persistent compunit.cu_name :: defined)
        (ofs + size) rem

(* Generate the code that builds the tuple representing the package
   module:
     GETGLOBAL M.An
     PUSHGETGLOBAL M.An-1
     ...
     PUSHGETGLOBAL M.A1
     MAKEBLOCK tag = 0 size = n
     SETGLOBAL M
*)

let build_global_target oc target_name mapping ofs =
  let out_word n =
    output_byte oc n;
    output_byte oc (n lsr 8);
    output_byte oc (n lsr 16);
    output_byte oc (n lsr 24) in
  let rec build_global first pos = function
    [] ->
      out_word opMAKEBLOCK;             (* pos *)
      out_word (List.length mapping);   (* pos + 4 *)
      out_word 0;                       (* pos + 8 *)
      out_word opSETGLOBAL;             (* pos + 12 *)
      out_word 0;                       (* pos + 16 *)
      relocs := (Reloc_setglobal target_name, pos + 16) :: !relocs
  | (oldname, newname) :: rem ->
      out_word (if first then opGETGLOBAL else opPUSHGETGLOBAL); (* pos *)
      out_word 0;                       (* pos + 4 *)
      relocs := (Reloc_getglobal newname, pos + 4) :: !relocs;
      build_global false (pos + 8) rem in
  build_global true ofs (List.rev mapping)

(* Build the .cmo file obtained by packaging the given .cmo files. *)

let package_object_files objfiles targetfile targetname =
  let units =
    List.map (fun f -> (f, read_unit_info f)) objfiles in
  let unit_names =
    List.map (fun (_, cu) -> cu.cu_name) units in
  let mapping =
    List.map
      (fun name ->
          (Ident.create_persistent name,
           Ident.create_persistent(targetname ^ "." ^ name)))
      unit_names in
  let oc = open_out_bin targetfile in
  try
    output_string oc Config.cmo_magic_number;
    let pos_depl = pos_out oc in
    output_binary_int oc 0;
    let pos_code = pos_out oc in
    let ofs = rename_append_bytecode_list oc mapping [] 0 units in
    build_global_target oc (Ident.create_persistent targetname) mapping ofs;
    let pos_debug = pos_out oc in
    if !Clflags.debug && !events <> [] then output_value oc (List.rev !events);
    let pos_final = pos_out oc in
    let imports =
      List.filter
        (fun (name, crc) -> not (List.mem name unit_names))
        (Bytelink.extract_crc_interfaces()) in
    let compunit =
      { cu_name = targetname;
        cu_pos = pos_code;
        cu_codesize = pos_debug - pos_code;
        cu_reloc = List.rev !relocs;
        cu_imports = (targetname, Env.crc_of_unit targetname) :: imports;
        cu_primitives = !primitives;
        cu_force_link = !force_link;
        cu_debug = if pos_final > pos_debug then pos_debug else 0;
        cu_debugsize = pos_final - pos_debug } in
    output_value oc compunit;
    seek_out oc pos_depl;
    output_binary_int oc pos_final;
    close_out oc
  with x ->
    close_out oc;
    raise x

(* The entry point *)

let package_files files targetfile =
  let objfiles =
    List.map
      (fun f ->
        try find_in_path !Config.load_path f
        with Not_found -> raise(Error(File_not_found f)))
      files in
  let prefix = chop_extension_if_any targetfile in
  let targetcmi = prefix ^ ".cmi" in
  let targetname = String.capitalize(Filename.basename prefix) in
  try
    Typemod.package_units objfiles targetcmi targetname;
    package_object_files objfiles targetfile targetname
  with x ->
    remove_file targetcmi; remove_file targetfile; raise x

(* Error report *)

open Format

let report_error ppf = function
    Forward_reference(file, ident) ->
      fprintf ppf "Forward reference to %s in file %s" (Ident.name ident) file
  | Multiple_definition(file, ident) ->
      fprintf ppf "File %s redefines %s" file (Ident.name ident)
  | Not_an_object_file file ->
      fprintf ppf "%s is not a bytecode object file" file
  | Illegal_renaming(file, id) ->
      fprintf ppf "Wrong file naming: %s@ contains the code for@ %s"
        file id
  | File_not_found file ->
      fprintf ppf "File %s not found" file
