%-----------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%-----------------------------------------------------------------------------%
% Copyright (C) 2008, 2010-2011 The University of Melbourne.
% This file may only be copied under the terms of the GNU General
% Public License - see the file COPYING in the Mercury distribution.
%-----------------------------------------------------------------------------%
%
% File: pickle.m
% Main authors: petdr, wangp.
%
% This file contains routines to serialise arbitrary data structures into some
% unspecified binary format which can be restored quickly.
%
% We don't preserve sharing in the pickled data structure. This would be
% possible, but would introduce slowdowns in both the pickling and unpickling
% processes.
%
%-----------------------------------------------------------------------------%
%-----------------------------------------------------------------------------%

:- module libs.pickle.
:- interface.

:- import_module io.
:- import_module type_desc.
:- import_module univ.

%-----------------------------------------------------------------------------%

    % A type which holds custom pickling predicates.
    %
:- type picklers.

:- type pickler_pred == pred(io.binary_output_stream, picklers, univ, io, io).
:- inst pickler_pred == (pred(in, in, in, di, uo) is det).

    % Initialize the custom pickling predicates.
    %
:- func init_picklers = picklers.

    % For the type described by the type_ctor_desc, add the supplied custom
    % pickle predicate to the set of registered pickle predicates.
    %
:- pred register_pickler(type_ctor_desc::in, pickler_pred::in(pickler_pred),
    picklers::in, picklers::out) is det.

    % Serialise an arbitrary data structure into the current binary output
    % stream, using the picklers given to override the default pickling method.
    %
    % Existential, foreign and higher-order types are not supported
    % generically. Register custom handlers to handle those types.
    %
:- pred pickle(io.binary_output_stream::in, picklers::in, T::in,
    io::di, io::uo) is det.

%-----------------------------------------------------------------------------%

    % A type which holds the custom unpickling predicates.
    %
:- type unpicklers.

:- type unpickle_handle.

:- type unpickle_state.

:- type unpickler_pred == pred(unpicklers, unpickle_handle, type_desc, univ,
                            unpickle_state, unpickle_state).
:- inst unpickler_pred == (pred(in, in, in, out, di, uo) is det).

    % Initialize the custom unpickling predicates.
    %
:- func init_unpicklers = unpicklers.

    % For the type described by the type_ctor_desc, add the supplied custom
    % unpickle predicate to the set of registered unpickle predicates.
    %
:- pred register_unpickler(type_ctor_desc::in,
    unpickler_pred::in(unpickler_pred), unpicklers::in, unpicklers::out)
    is det.

    % Get a pickled type back out from the file, using the unpicklers to
    % override the default unpickling method.
    %
:- pred unpickle_from_file(unpicklers::in, string::in, io.res(T)::out,
    io::di, io::uo) is det.

    % Unpickle a single value.
    %
:- pred unpickle(unpicklers::in, unpickle_handle::in, T::out,
    unpickle_state::di, unpickle_state::uo) is det.

%-----------------------------------------------------------------------------%
%-----------------------------------------------------------------------------%

:- implementation.

:- import_module bitmap.
:- import_module bool.
:- import_module char.
:- import_module construct.
:- import_module deconstruct.
:- import_module exception.
:- import_module int.
:- import_module list.
:- import_module map.
:- import_module require.
:- import_module string.

%-----------------------------------------------------------------------------%

:- type picklers
    --->    picklers(
                map(type_ctor_desc, pickler_pred)
            ).

:- type unpicklers
    --->    unpicklers(
                map(type_ctor_desc, unpickler_pred)
            ).

:- type unpickle_handle == bitmap.
:- type unpickle_state  == int.         % offset into bitmap

:- type get_byte_out_of_range
    --->    get_byte_out_of_range(string).

%-----------------------------------------------------------------------------%
%
% Pickling.
%

init_picklers = picklers(map.init).

register_pickler(TypeCtorDesc, Pickle, Pickles0, Pickles) :-
    Pickles0 = picklers(Map0),
    map.det_insert(TypeCtorDesc, Pickle, Map0, Map),
    Pickles = picklers(Map).

pickle(OutputStream, Pickles, T, !IO) :-
    ( if
        dynamic_cast(T, String)
    then
        pickle_string(OutputStream, String, !IO)
    else if
        dynamic_cast(T, Int)
    then
        pickle_int32(OutputStream, Int, !IO)
    else if
        dynamic_cast(T, Float)
    then
        pickle_float(OutputStream, Float, !IO)
    else if
        dynamic_cast(T, Char)
    then
        pickle_char(OutputStream, Char, !IO)
    else if
        TypeDesc = type_of(T),
        TypeCtorDesc = type_ctor(TypeDesc),
        user_defined_pickler(Pickles, TypeCtorDesc, Pickle)
    then
        Pickle(OutputStream, Pickles, univ(T), !IO)
    else
        deconstruct.functor(T, do_not_allow, Functor, Arity),
        pickle_string(OutputStream, Functor, !IO),
        pickle_int32(OutputStream, Arity, !IO),
        pickle_args(OutputStream, Pickles, 0, Arity, T, !IO)
    ).

:- pred pickle_args(io.binary_output_stream::in, picklers::in,
    int::in, int::in, T::in, io::di, io::uo) is det.

pickle_args(OutputStream, Pickles, N, Arity, T, !IO) :-
    ( if N = Arity then
        true
    else
        ( if deconstruct.arg(T, do_not_allow, N, Arg) then
            pickle(OutputStream, Pickles, Arg, !IO),
            pickle_args(OutputStream, Pickles, N + 1, Arity, T, !IO)
        else
            unexpected($pred, "unable to deconstruct arg")
        )
    ).

:- pred user_defined_pickler(picklers::in, type_ctor_desc::in,
    pickler_pred::out(pickler_pred)) is semidet.

user_defined_pickler(picklers(Pickles), TypeCtorDesc, Pickle) :-
    map.search(Pickles, TypeCtorDesc, Pickle0),
    pickler_inst_cast(Pickle0, Pickle).

:- pred pickler_inst_cast(pickler_pred::in, pickler_pred::out(pickler_pred))
    is det.

:- pragma foreign_proc("C",
    pickler_inst_cast(A::in, B::out(pickler_pred)),
    [will_not_call_mercury, thread_safe, promise_pure],
"
    B = A;
").

:- pragma foreign_proc("Java",
    pickler_inst_cast(A::in, B::out(pickler_pred)),
    [will_not_call_mercury, thread_safe, promise_pure],
"
    B = A;
").

:- pragma foreign_proc("C#",
    pickler_inst_cast(A::in, B::out(pickler_pred)),
    [will_not_call_mercury, thread_safe, promise_pure],
"
    B = A;
").

%-----------------------------------------------------------------------------%
%
% Unpickling.
%

init_unpicklers = unpicklers(map.init).

register_unpickler(TypeCtorDesc, Unpickle, Unpicklers0, Unpicklers) :-
    Unpicklers0 = unpicklers(Map0),
    map.det_insert(TypeCtorDesc, Unpickle, Map0, Map),
    Unpicklers = unpicklers(Map).

unpickle_from_file(Unpicklers, FileName, Result, !IO) :-
    io.open_binary_input(FileName, FileResult, !IO),
    (
        FileResult = ok(FileStream),
        % Perform unpickling from an intermediate memory buffer,
        % as it seems to be faster.
        io.read_binary_file_as_bitmap(FileStream, ReadResult, !IO),
        io.close_binary_input(FileStream, !IO),
        (
            ReadResult = ok(Bitmap),
            promise_equivalent_solutions [TryResult]
            (
                try(
                    ( pred(T0::out) is det :-
                        unpickle(Unpicklers, Bitmap, T0, 0, _State)
                    ),
                    TryResult
                )
            ),
            (
                TryResult = succeeded(T),
                Result = ok(T)
            ;
                TryResult = exception(Excp),
                ( if univ_to_type(Excp, get_byte_out_of_range(Msg)) then
                    Result = error(io.make_io_error(Msg))
                else
                    rethrow(TryResult)
                )
            )
        ;
            ReadResult = error(Error),
            Result = error(Error)
        )
    ;
        FileResult = error(Error),
        Result = error(Error)
    ).

unpickle(Unpicklers, Handle, T, !State) :-
    unpickle_2(Unpicklers, Handle, type_of(T), Univ, !State),
    det_univ_to_type(Univ, T).

:- pred unpickle_2(unpicklers::in, unpickle_handle::in,
    type_desc::in, univ::out, unpickle_state::di, unpickle_state::uo)
    is det.

unpickle_2(Unpicklers, Handle, TypeDesc, Univ, !State) :-
    ( if
        TypeDesc = type_of(_ : string)
    then
        unpickle_string(Handle, String, !State),
        Univ = univ(String)
    else if
        TypeDesc = type_of(_ : int)
    then
        unpickle_int32(Handle, Int, !State),
        Univ = univ(Int)
    else if
        TypeDesc = type_of(_ : float)
    then
        unpickle_float(Handle, Float, !State),
        Univ = univ(Float)
    else if
        TypeDesc = type_of(_ : character)
    then
        unpickle_char(Handle, Char, !State),
        Univ = univ(Char)
    else if
        user_defined_unpickler(Unpicklers, type_ctor(TypeDesc), Unpickle)
    then
        Unpickle(Unpicklers, Handle, TypeDesc, Univ, !State)
    else
        unpickle_string(Handle, Functor, !State),
        unpickle_int32(Handle, Arity, !State),
        ( if
            ( if Functor = "{}" then
                IsTuple = yes,
                type_ctor_and_args(TypeDesc, _, ArgTypes),
                N = 0
            else
                IsTuple = no,
                % XXX consider tabling this call
                find_functor(TypeDesc, Functor, Arity, N, ArgTypes)
            )
        then
            list.map_foldl(unpickle_2(Unpicklers, Handle), ArgTypes, ArgUnivs,
                !State),
            (
                IsTuple = yes,
                Univ = construct_tuple(ArgUnivs)
            ;
                IsTuple = no,
                ( if construct(TypeDesc, N, ArgUnivs, Univ0) then
                    Univ = Univ0
                else
                    unexpected($pred, "unable to construct")
                )
            )
        else
            unexpected($pred, "unable to unpickle")
        )
    ).

:- pred user_defined_unpickler(unpicklers::in, type_ctor_desc::in,
    unpickler_pred::out(unpickler_pred)) is semidet.

user_defined_unpickler(unpicklers(Unpicklers), TypeCtorDesc, Unpickle) :-
    map.search(Unpicklers, TypeCtorDesc, Unpickle0),
    unpickler_inst_cast(Unpickle0, Unpickle).

:- pred unpickler_inst_cast(unpickler_pred::in,
    unpickler_pred::out(unpickler_pred)) is det.

:- pragma foreign_proc("C",
    unpickler_inst_cast(A::in, B::out(unpickler_pred)),
    [will_not_call_mercury, thread_safe, promise_pure],
"
    B = A;
").

:- pragma foreign_proc("Java",
    unpickler_inst_cast(A::in, B::out(unpickler_pred)),
    [will_not_call_mercury, thread_safe, promise_pure],
"
    B = A;
").

:- pragma foreign_proc("C#",
    unpickler_inst_cast(A::in, B::out(unpickler_pred)),
    [will_not_call_mercury, thread_safe, promise_pure],
"
    B = A;
").

%-----------------------------------------------------------------------------%
%
% Basic types picklers/unpicklers.
%

:- pred pickle_int32(io.binary_output_stream::in, int::in,
    io::di, io::uo) is det.

pickle_int32(OutputStream, Int, !IO) :-
    A = (Int >> 24) /\ 0xff,
    B = (Int >> 16) /\ 0xff,
    C = (Int >>  8) /\ 0xff,
    D = (Int >>  0) /\ 0xff,
    io.write_byte(OutputStream, A, !IO),
    io.write_byte(OutputStream, B, !IO),
    io.write_byte(OutputStream, C, !IO),
    io.write_byte(OutputStream, D, !IO).

:- pred unpickle_int32(unpickle_handle::in, int::out,
    unpickle_state::di, unpickle_state::uo) is det.

unpickle_int32(Handle, Int, !State) :-
    get_byte(Handle, A, !State),
    get_byte(Handle, B, !State),
    get_byte(Handle, C, !State),
    get_byte(Handle, D, !State),
    Int0 = (A `unchecked_left_shift` 24)
        \/ (B `unchecked_left_shift` 16)
        \/ (C `unchecked_left_shift`  8)
        \/ (D `unchecked_left_shift`  0),
    Int = sign_extend_32(Int0).

:- func sign_extend_32(int) = int.

sign_extend_32(X) = R :-
    % http://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend
    Mask = 1 `unchecked_left_shift` 31,
    R = (X `xor` Mask) - Mask.

:- pred pickle_char(io.binary_output_stream::in, char::in,
    io::di, io::uo) is det.

pickle_char(OutputStream, Char, !IO) :-
    % XXX handle non-ASCII characters
    char.to_int(Char, Int),
    io.write_byte(OutputStream, Int, !IO).

:- pred unpickle_char(unpickle_handle::in, char::out,
    unpickle_state::di, unpickle_state::uo) is det.

unpickle_char(Handle, Char, !State) :-
    get_byte(Handle, Byte, !State),
    char.det_from_int(Byte, Char).

:- pred pickle_string(io.binary_output_stream::in, string::in,
    io::di, io::uo) is det.

pickle_string(OutputStream, String, !IO) :-
    Length = string.length(String),
    pickle_int32(OutputStream, Length, !IO),
    string.foldl(pickle_char(OutputStream), String, !IO).

:- pred unpickle_string(unpickle_handle::in, string::uo,
    unpickle_state::di, unpickle_state::uo) is det.

unpickle_string(Handle, String, !State) :-
    unpickle_int32(Handle, Length, !State),
    allocate_string(Length, String0),
    unpickle_string_2(Handle, 0, Length, String0, String, !State).

:- pred unpickle_string_2(unpickle_handle::in, int::in, int::in,
    string::di, string::uo, unpickle_state::di, unpickle_state::uo) is det.

unpickle_string_2(Handle, Index, Length, !String, !State) :-
    ( if Index = Length then
        true
    else
        unpickle_char(Handle, Char, !State),
        local_unsafe_set_char(Char, Index, !String),
        unpickle_string_2(Handle, Index + 1, Length, !String, !State)
    ).

:- pred allocate_string(int::in, string::uo) is det.
:- pragma no_determinism_warning(pred(allocate_string/2)).

:- pragma foreign_proc("C",
    allocate_string(Length::in, Str::uo),
    [will_not_call_mercury, promise_pure, thread_safe],
"
    MR_allocate_aligned_string_msg(Str, Length, MR_ALLOC_ID);
    Str[Length] = '\\0';
").

allocate_string(_, _) :-
    sorry($file, $pred).

    % string.unsafe_set_char is disabled in the standard library so we need our
    % own copy.
:- pred local_unsafe_set_char(char::in, int::in, string::di, string::uo)
    is det.
:- pragma no_determinism_warning(pred(local_unsafe_set_char/4)).

:- pragma foreign_proc("C",
    local_unsafe_set_char(Chr::in, Index::in, Str0::di, Str::uo),
    [will_not_call_mercury, promise_pure, thread_safe],
"
    Str0[Index] = Chr;
    Str = Str0;
").

local_unsafe_set_char(_, _, _, _) :-
    sorry($file, $pred).

:- pred pickle_float(io.binary_output_stream::in, float::in,
    io::di, io::uo) is det.

pickle_float(OutputStream, Float, !IO) :-
    reinterpret_float_as_ints(Float, A, B),
    % We always write floats using 64 bits. Single precision floats are not
    % the default and the compiler hardly uses floats anyhow.
    pickle_int32(OutputStream, A, !IO),
    pickle_int32(OutputStream, B, !IO).

:- pred reinterpret_float_as_ints(float::in, int::out, int::out) is det.
:- pragma no_determinism_warning(pred(reinterpret_float_as_ints/3)).

:- pragma foreign_proc("C",
    reinterpret_float_as_ints(Flt::in, A::out, B::out),
    [will_not_call_mercury, promise_pure, thread_safe],
"
    if (sizeof(MR_Float) == sizeof(float)) {
        MR_uint_least32_t   *p;

        p = (MR_uint_least32_t *) &Flt;
        A = *p;
        B = 0;
    } else {
        MR_uint_least64_t   *p;

        p = (MR_uint_least64_t *) &Flt;
        A = (*p >> 32) & 0xffffffff;
        B = (*p >>  0) & 0xffffffff;
    }
").

reinterpret_float_as_ints(_, _, _) :-
    sorry($file, $pred).

:- pred unpickle_float(unpickle_handle::in, float::out,
    unpickle_state::di, unpickle_state::uo) is det.

unpickle_float(Handle, Float, !State) :-
    unpickle_int32(Handle, A, !State),
    unpickle_int32(Handle, B, !State),
    reinterpret_ints_as_float(A, B, Float).

:- pred reinterpret_ints_as_float(int::in, int::in, float::out) is det.
:- pragma no_determinism_warning(pred(reinterpret_ints_as_float/3)).

:- pragma foreign_proc("C",
    reinterpret_ints_as_float(A::in, B::in, Flt::out),
    [will_not_call_mercury, promise_pure, thread_safe],
"
    if (sizeof(MR_Float) == sizeof(float)) {
        MR_Float    *p;

        p = (MR_Float *) &A;
        Flt = *p;
        (void) B;
    } else {
        MR_uint_least64_t   tmp;
        MR_Float            *p;

        tmp = (((MR_uint_least64_t) A) << 32) |
            (((MR_uint_least64_t) B) & 0xffffffff);
        p = (MR_Float *) &tmp;
        Flt = *p;
    }
").

reinterpret_ints_as_float(_, _, _) :-
    sorry($file, $pred).

:- pred get_byte(unpickle_handle::in, int::out,
    unpickle_state::di, unpickle_state::uo) is det.

get_byte(Bitmap, Byte, Index, Index + 1) :-
    ( if bitmap.byte_in_range(Bitmap, Index) then
        Byte = Bitmap ^ unsafe_byte(Index)
    else
        Msg = "byte " ++ string.from_int(Index) ++ " is out of range",
        throw(get_byte_out_of_range(Msg))
    ).

%-----------------------------------------------------------------------------%
:- end_module libs.pickle.
%-----------------------------------------------------------------------------%
