Current section

Files

Jump to
gpb src gpb_lib.erl
Raw

src/gpb_lib.erl

%%% Copyright (C) 2017 Tomas Abrahamsson
%%%
%%% Author: Tomas Abrahamsson <tab@lysator.liu.se>
%%%
%%% This library is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU Lesser General Public
%%% License as published by the Free Software Foundation; either
%%% version 2.1 of the License, or (at your option) any later version.
%%%
%%% This library is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% Lesser General Public License for more details.
%%%
%%% You should have received a copy of the GNU Lesser General Public
%%% License along with this library; if not, write to the Free Software
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
%%% MA 02110-1301 USA
%%% @doc Helper functions for the code-generator module
%%% @private
-module(gpb_lib).
-include("gpb_codegen.hrl").
-include("gpb_compile.hrl").
-export([mk_fn/2, mk_fn/3]).
-export([replace_term/2]).
-export([replace_tree/2]).
-export([splice_trees/2]).
-export([repeat_clauses/2]).
-export([msgs_or_groups/1]).
-export([msg_or_group_names/1]).
-export([msg_names/1]).
-export([contains_messages/1]).
-export([get_field_name/1, get_field_names/1]).
-export([get_field_rnum/1]).
-export([get_field_occurrence/1]).
-export([map_type_to_msg_name/2]).
-export([unalias_enum/1]).
-export([zip_for_non_opt_fields/2]).
-export([any_field_is_sub_msg/1]).
-export([any_field_is_repeated/1]).
-export([any_enum_field_exists/1]).
-export([any_packed_field_exists/1]).
-export([at_least_one_submsg_with_size_not_known_at_compile_time_exists/1]).
-export([get_field_pass/2]).
-export([get_num_fields/2]).
-export([is_packed/1]).
-export([key_partition_on_optionality/2, key_partition_on_optionality/3]).
-export([classify_field_merge_action/1]).
-export([flatten_oneof_fields/1]).
-export([fold_msg_fields/3]).
-export([fold_msg_or_group_fields/3]).
-export([fold_msgdef_fields/3]).
-export([fold_msg_or_group_fields_o/3]).
-export([mapping_match/3]).
-export([mapping_create/3]).
-export([mapping_create/4]).
-export([mapping_update/4]).
-export([record_match/2]).
-export([record_create/2]).
-export([record_update/3]).
-export([map_match/2]).
-export([map_create/2]).
-export([map_set/3]).
-export([get_2tuples_or_maps_for_maptype_fields_by_opts/1]).
-export([get_records_or_maps_by_opts/1]).
-export([get_mapping_and_unset_by_opts/1]).
-export([get_strings_as_binaries_by_opts/1]).
-export([get_type_specs_by_opts/1]).
-export([get_gen_descriptor_by_opts/1]).
-export([get_field_format_by_opts/1]).
-export([mk_get_defs_as_maps_or_records_fn/1]).
-export([get_defs_as_maps_or_records/1]).
-export([get_epb_functions_by_opts/1]).
-export([is_target_major_version_at_least/2]).
-export([target_has_lists_join/1]).
-export([target_has_variable_key_map_update/1]).
-export([target_can_specify_map_item_presence_in_typespecs/1]).
-export([target_can_do_flat_oneof_for_maps/1]).
-export([target_may_fail_compilation_for_flat_oneof_for_maps/1]).
-export([target_has_stacktrace_syntax/1]).
-export([current_otp_release/0]).
-export([proto2_type_default/3]).
-export([proto3_type_default/3]).
-export([get_maps_key_type_by_opts/1]).
-export([var_f_n/1]).
-export([var_b_n/1]).
-export([var_n/2]).
-export([var/2]).
-export([prefix_var/2]).
-export([assign_to_var/2]).
-export([match_bind_var/2]).
-export([varint_to_binary_fields/1]).
-export([do_exprs/3]).
-export([index_seq/1]).
-export([smember/2, smember_any/2]).
-export([indent/2, indent_lines/2]).
-export([outdent_first/1]).
-export([split_indent_iolist/2]).
-export([split_indent_butfirst_iolist/2]).
-export([cond_split_indent_iolist/3]).
-export([iolist_to_utf8_or_escaped_binary/2]).
-export([nowarn_unused_function/2]).
-export([nowarn_dialyzer_attr/3]).
-export([comma_join/1]).
-export([nl_join/1]).
-export([or_join/1]).
-export([dot_join/1]).
-export([is_substr/2]).
-export([string_slice/2]).
-export([string_lexemes/2]).
-export([lowercase/1]).
-export([uppercase/1]).
-export([snake_case/1]).
-include("../include/gpb.hrl").
mk_fn(Prefix, Suffix) ->
list_to_atom(lists:concat([Prefix, Suffix])).
mk_fn(Prefix, Middlefix, Suffix) when is_integer(Middlefix) ->
mk_fn(Prefix, list_to_atom(integer_to_list(Middlefix)), Suffix);
mk_fn(Prefix, Middlefix, Suffix) ->
list_to_atom(lists:concat([Prefix, Middlefix, "_", Suffix])).
%% Helpers for gpb_codegen parse tree transform operations -----------
replace_term(Marker, NewTerm) when is_atom(Marker) ->
{replace_term, Marker, NewTerm}.
replace_tree(Marker, NewTree) when is_atom(Marker) ->
{replace_tree, Marker, NewTree}.
splice_trees(Marker, Trees) when is_atom(Marker) ->
{splice_trees, Marker, Trees}.
repeat_clauses(Marker, RepetitionReplacements) ->
{repeat_clauses, Marker, RepetitionReplacements}.
%% Various accessors -----
msgs_or_groups(Defs) ->
[{Type,Name,Fields} || {{Type,Name},Fields} <- Defs,
Type =:= msg orelse Type =:= group].
msg_or_group_names(Defs) ->
[Name || {_Type, Name, _Fields} <- msgs_or_groups(Defs)].
msg_names(Defs) ->
[Name || {{msg, Name}, _Fields} <- Defs].
contains_messages(Defs) ->
lists:any(fun({{msg, _}, _}) -> true;
(_) -> false
end,
Defs).
get_field_names(MsgDef) ->
[get_field_name(Field) || Field <- MsgDef].
get_field_name(#?gpb_field{name=FName}) -> FName;
get_field_name(#gpb_oneof{name=FName}) -> FName.
get_field_rnum(#?gpb_field{rnum=RNum}) -> RNum;
get_field_rnum(#gpb_oneof{rnum=RNum}) -> RNum.
get_field_occurrence(#?gpb_field{occurrence=Occurrence}) -> Occurrence;
get_field_occurrence(#gpb_oneof{}) -> optional.
map_type_to_msg_name(KeyType, {msg,MsgName}) ->
list_to_atom(?ff("map<~s,~s>", [KeyType, MsgName]));
map_type_to_msg_name(KeyType, {enum,EnumName}) ->
list_to_atom(?ff("map<~s,~s>", [KeyType, EnumName]));
map_type_to_msg_name(KeyType, ValueType) ->
list_to_atom(?ff("map<~s,~s>", [KeyType, ValueType])).
%% The "option allow_alias = true;" inside an enum X { ... }
%% says it is ok to have multiple symbols that map to the same numeric value.
%% Appeared in protobuf 2.5.0.
unalias_enum([{_Sym,Value}=Enum | Rest]) ->
[Enum | unalias_enum([E || {_,V}=E <- Rest, V /= Value])];
unalias_enum([{option,_Name,_Value} | Rest]) ->
unalias_enum(Rest);
unalias_enum([]) ->
[].
zip_for_non_opt_fields([#?gpb_field{name=FName,
occurrence=Occurrence} | FRest],
[Elem | ERest]) ->
case Occurrence of
optional -> zip_for_non_opt_fields(FRest, ERest);
required -> [{FName, Elem} | zip_for_non_opt_fields(FRest, ERest)];
repeated -> zip_for_non_opt_fields(FRest, ERest)
end;
zip_for_non_opt_fields([#gpb_oneof{} | FRest], [_Elem | ERest]) ->
zip_for_non_opt_fields(FRest, ERest);
zip_for_non_opt_fields([], []) ->
[].
any_field_is_sub_msg(Fields) ->
lists:any(fun(#?gpb_field{type={msg,_}}) -> true;
(#?gpb_field{type={group,_}}) -> true;
(#?gpb_field{type={map,_,_}}) -> true;
(#gpb_oneof{fields=Fs}) -> any_field_is_sub_msg(Fs);
(_) -> false
end,
Fields).
any_field_is_repeated(Fields) ->
lists:any(fun(#?gpb_field{occurrence=Occ}) -> Occ == repeated;
(#gpb_oneof{}) -> false
end,
Fields).
any_enum_field_exists(UsedTypes) ->
sets:fold(fun({enum,_}, _Acc) -> true;
(_, Acc) -> Acc
end,
false,
UsedTypes).
any_packed_field_exists(#anres{num_packed_fields=0}) -> false;
any_packed_field_exists(#anres{num_packed_fields=_}) -> true.
at_least_one_submsg_with_size_not_known_at_compile_time_exists(AnRes) ->
#anres{used_types=UsedTypes,
maps_as_msgs=MapsAsMsgs,
known_msg_size=KnownSize} = AnRes,
SubMsgNames = [MsgName || {msg,MsgName} <- sets:to_list(UsedTypes)],
MapMsgNames = [MsgName || {{msg,MsgName},_} <- MapsAsMsgs],
IsMsgSizeUnknown = fun(Nm) -> dict:fetch(Nm, KnownSize) == undefined end,
lists:any(IsMsgSizeUnknown, SubMsgNames) orelse
lists:any(IsMsgSizeUnknown, MapMsgNames).
get_field_pass(MsgName, #anres{d_field_pass_method=D}) ->
dict:fetch(MsgName, D).
get_num_fields(MsgName, #anres{num_fields=D}) ->
dict:fetch(MsgName, D).
is_packed(#?gpb_field{type=Type, opts=Opts}) ->
gpb:is_type_packable(Type) andalso lists:member(packed, Opts).
is_maptype_field(#?gpb_field{type={map,_,_}}) -> true;
is_maptype_field(_) -> false.
%% -> {Optionals, NonOptionals}
key_partition_on_optionality(Key, Items) ->
key_partition_on_optionality(Key, Items, []).
key_partition_on_optionality(Key, Items, Opts) ->
lists:partition(
fun(Item) ->
Field = element(Key, Item),
case get_field_occurrence(Field) of
optional -> true;
required -> false;
repeated -> case mapfields_considered_required(Opts) of
false -> true;
true -> not is_maptype_field(Field)
end
end
end,
Items).
mapfields_considered_required(Opts) ->
proplists:get_bool(mapfields_are_required, Opts).
classify_field_merge_action(FieldDef) ->
case FieldDef of
#?gpb_field{occurrence=required, type={msg, _}} -> msgmerge;
#?gpb_field{occurrence=optional, type={msg, _}} -> msgmerge;
#?gpb_field{occurrence=required, type={group, _}} -> msgmerge;
#?gpb_field{occurrence=optional, type={group, _}} -> msgmerge;
#?gpb_field{occurrence=required} -> overwrite;
#?gpb_field{occurrence=optional} -> overwrite;
#?gpb_field{occurrence=repeated} -> seqadd
end.
flatten_oneof_fields([#?gpb_field{}=F | Rest]) ->
[F | flatten_oneof_fields(Rest)];
flatten_oneof_fields([#gpb_oneof{fields=OFields} | Rest]) ->
OFields ++ flatten_oneof_fields(Rest);
flatten_oneof_fields([]) ->
[].
%% Msg iteration --------
%% Loop over all message fields, including oneof-fields
%% Call Fun for all #?gpb_fields{}, skip over non-msg defs
fold_msg_fields(Fun, InitAcc, Defs) ->
fold_msg_fields_o(
fun(MsgName, Field, _IsOneOf, Acc) -> Fun(MsgName, Field, Acc) end,
InitAcc,
Defs).
fold_msg_or_group_fields(Fun, InitAcc, Defs) ->
fold_msg_or_group_fields_o(
fun(Type, Name, Field, _IsOneOf, Acc) -> Fun(Type, Name, Field, Acc) end,
InitAcc,
Defs).
fold_msgdef_fields(Fun, InitAcc, Fields) ->
fold_msgdef_fields_o(
fun(Field, _IsOneOf, Acc) -> Fun(Field, Acc) end,
InitAcc,
Fields).
%% The fun takes 4 args: Fun(Msgname, #?gpb_field{}, IsOneof, Acc) -> Acc1
fold_msg_fields_o(Fun, InitAcc, Defs) ->
lists:foldl(
fun({{msg, MsgName}, Fields}, Acc) ->
FFun = fun(Field, IsOneOf, FAcc) ->
Fun(MsgName, Field, IsOneOf, FAcc)
end,
fold_msgdef_fields_o(FFun, Acc, Fields);
(_Def, Acc) ->
Acc
end,
InitAcc,
Defs).
%% The fun takes 5 args:
%% Fun(msg | group, Name, #?gpb_field{}, IsOneof, Acc) -> Acc1
fold_msg_or_group_fields_o(Fun, InitAcc, Defs) ->
lists:foldl(
fun({{Type, Name}, Fields}, Acc) when Type =:= msg;
Type =:= group ->
FFun = fun(Field, IsOneOf, FAcc) ->
Fun(Type, Name, Field, IsOneOf, FAcc)
end,
fold_msgdef_fields_o(FFun, Acc, Fields);
(_Def, Acc) ->
Acc
end,
InitAcc,
Defs).
%% The fun takes 3 args: Fun(#?gpb_field{}, IsOneof, Acc) -> Acc1
fold_msgdef_fields_o(Fun, InitAcc, Fields) ->
lists:foldl(
fun(#?gpb_field{}=Field, Acc) ->
Fun(Field, false, Acc);
(#gpb_oneof{name=CFName, fields=OFields}, Acc) ->
IsOneOf = {true, CFName},
lists:foldl(fun(OField, OAcc) -> Fun(OField, IsOneOf, OAcc) end,
Acc,
OFields)
end,
InitAcc,
Fields).
%% Record or map expr helpers --------
%% a mapping is either a record or a map
%%
%%
mapping_match(RName, Fields, Opts) ->
case get_records_or_maps_by_opts(Opts) of
records -> record_match(RName, Fields);
maps -> map_match(Fields, Opts)
end.
mapping_create(RName, Fields, Opts) when is_list(Opts) ->
Fn = fun() -> get_records_or_maps_by_opts(Opts) end,
mapping_create(RName, Fields, Fn, Opts).
mapping_create(RName, Fields, RecordsOrMaps, Opts)
when is_function(RecordsOrMaps) ->
case RecordsOrMaps() of
records -> record_create(RName, Fields);
maps -> map_create(Fields, Opts)
end.
mapping_update(Var, RName, FieldsValues, Opts) ->
case get_records_or_maps_by_opts(Opts) of
records ->
record_update(Var, RName, FieldsValues);
maps ->
case get_mapping_and_unset_by_opts(Opts) of
#maps{unset_optional=present_undefined} ->
map_update(Var, FieldsValues, Opts);
#maps{unset_optional=omitted} ->
map_set(Var, FieldsValues, Opts)
end
end.
%% records
record_match(RName, Fields) -> record_create_or_match(RName, Fields).
record_create(RName, Fields) -> record_create_or_match(RName, Fields).
record_create_or_match(RecordName, FieldsValueTrees) ->
record_update(none, RecordName, FieldsValueTrees).
record_update(Var, _RecordName, []) when Var /= none ->
%% No updates to be made, maybe no fields
Var;
record_update(Var, RecordName, FieldsValueTrees) ->
erl_syntax:record_expr(
Var,
erl_syntax:atom(RecordName),
[erl_syntax:record_field(erl_syntax:atom(FName), ValueSyntaxTree)
|| {FName, ValueSyntaxTree} <- FieldsValueTrees]).
%% maps
-ifndef(NO_HAVE_MAPS).
map_match(Fields, Opts) ->
Literal = mapkey_literal_by_opts(Opts),
erl_syntax:map_expr(
[erl_syntax:map_field_exact(Literal(FName), Expr)
|| {FName, Expr} <- Fields]).
map_create(Fields, Opts) ->
map_set(none, Fields, Opts).
map_update(Var, [], _Opts) when Var /= none ->
%% No updates to be made, maybe no fields
Var;
map_update(Var, FieldsValueTrees, Opts) ->
Literal = mapkey_literal_by_opts(Opts),
erl_syntax:map_expr(
Var,
[erl_syntax:map_field_exact(Literal(FName), Expr)
|| {FName, Expr} <- FieldsValueTrees]).
map_set(Var, [], _Opts) when Var /= none ->
%% No updates to be made, maybe no fields
Var;
map_set(Var, FieldsValueTrees, Opts) ->
Literal = mapkey_literal_by_opts(Opts),
ExprF = mapkey_expr_by_opts(Opts),
erl_syntax:map_expr(
Var,
[if is_atom(FName) ->
erl_syntax:map_field_assoc(Literal(FName), Expr);
true -> % Key can be a variable or other type too.
erl_syntax:map_field_assoc(ExprF(FName), Expr)
end
|| {FName, Expr} <- FieldsValueTrees]).
mapkey_literal_by_opts(Opts) ->
case get_maps_key_type_by_opts(Opts) of
atom ->
fun(Atom) -> erl_syntax:atom(Atom) end;
binary ->
fun(Atom) ->
erl_syntax:binary(
[erl_syntax:binary_field(
erl_syntax:string(atom_to_list(Atom)))])
end
end.
mapkey_expr_by_opts(Opts) ->
case get_maps_key_type_by_opts(Opts) of
atom ->
fun(Expr) -> Expr end;
binary ->
fun(Expr) ->
erl_syntax:binary(
[erl_syntax:binary_field(
erl_syntax:application(erl_syntax:atom(erlang),
erl_syntax:atom(atom_to_list),
[Expr]))])
end
end.
-else. %% on a pre Erlang 17 system
map_match(Fields, Opts) ->
KVs = case get_maps_key_type_by_opts(Opts) of
atom ->
[?ff("~p := ~s", [FName, Var])
|| {FName, Var} <- map_kvars(Fields)];
binary ->
[?ff("<<\"~s\">> := ~s", [FName, Var])
|| {FName, Var} <- map_kvars(Fields)]
end,
erl_syntax:text(?ff("#{~s}", [string:join(KVs, ", ")])).
map_create(Fields, Opts) ->
KVs = case get_maps_key_type_by_opts(Opts) of
atom ->
[?ff("~p => ~s", [FName, Val])
|| {FName, Val} <- map_kvalues(Fields)];
binary ->
[?ff("<<\"~s\">> => ~s", [FName, Val])
|| {FName, Val} <- map_kvalues(Fields)]
end,
erl_syntax:text(?ff("#{~s}", [string:join(KVs, ", ")])).
map_update(Var, [], _Opts) when Var /= none ->
%% No updates to be made, maybe no fields
Var;
map_update(Var, FieldsValueTrees, Opts) ->
KVs = case get_maps_key_type_by_opts(Opts) of
atom ->
[?ff("~p := ~s", [FName, Val])
|| {FName, Val} <- map_kvalues(FieldsValueTrees)];
binary ->
[?ff("<<\"~s\">> := ~s", [FName, Val])
|| {FName, Val} <- map_kvalues(FieldsValueTrees)]
end,
erl_syntax:text(?ff("~s#{~s}", [var_literal(Var), string:join(KVs, ", ")])).
map_set(Var, [], _Opts) when Var /= none ->
%% No updates to be made, maybe no fields
Var;
map_set(Var, FieldsValueTrees, Opts) ->
KVs = case get_maps_key_type_by_opts(Opts) of
atom ->
[?ff("~p => ~s", [FName, Val])
|| {FName, Val} <- map_kvalues(FieldsValueTrees)];
binary ->
[?ff("<<\"~s\">> => ~s", [FName, Val])
|| {FName, Val} <- map_kvalues(FieldsValueTrees)]
end,
erl_syntax:text(?ff("~s#{~s}", [var_literal(Var), string:join(KVs, ", ")])).
%% -> [{atom(), string()}]
map_kvars(KVars) ->
[{Key, var_literal(Var)} || {Key, Var} <- KVars].
var_literal(Var) ->
variable = erl_syntax:type(Var),
erl_syntax:variable_literal(Var).
%% -> [{atom(), string()}]
map_kvalues(KVars) ->
[begin
ExprAsStr = erl_prettypr:format(Expr),
{Key, ExprAsStr}
end
|| {Key, Expr} <- KVars].
-endif. %% NO_HAVE_MAPS
%% Option helpers ---------------
get_2tuples_or_maps_for_maptype_fields_by_opts(Opts) ->
Default = false,
case proplists:get_value(mapfields_as_maps, Opts, Default) of
true -> maps;
false -> '2tuples'
end.
get_records_or_maps_by_opts(Opts) ->
Default = false,
case proplists:get_value(msgs_as_maps, Opts, Default) of
false -> records;
true -> maps
end.
get_mapping_and_unset_by_opts(Opts) ->
case get_records_or_maps_by_opts(Opts) of
records ->
records;
maps ->
DefaultUnsetOptional = omitted,
UnseOptional = proplists:get_value(maps_unset_optional, Opts,
DefaultUnsetOptional),
Oneof = proplists:get_value(maps_oneof, Opts, tuples),
#maps{unset_optional=UnseOptional, oneof=Oneof}
end.
get_strings_as_binaries_by_opts(Opts) ->
proplists:get_bool(strings_as_binaries, Opts).
get_type_specs_by_opts(Opts) ->
Default = true,
proplists:get_value(type_specs, Opts, Default).
get_gen_descriptor_by_opts(Opts) ->
proplists:get_bool(descriptor, Opts).
get_field_format_by_opts(Opts) ->
case proplists:get_bool(defs_as_proplists, proplists:unfold(Opts)) of
false -> %% default
case get_defs_as_maps_or_records(Opts) of
records -> fields_as_records;
maps -> fields_as_maps
end;
true ->
fields_as_proplists
end.
mk_get_defs_as_maps_or_records_fn(Opts) ->
fun() -> get_defs_as_maps_or_records(Opts) end.
get_defs_as_maps_or_records(Opts) ->
Default = false,
case proplists:get_value(defs_as_maps, Opts, Default) of
false -> records;
true -> maps
end.
get_epb_functions_by_opts(Opts) ->
proplists:get_bool(epb_functions, Opts).
is_target_major_version_at_least(VsnMin, Opts) ->
case proplists:get_value(target_erlang_version, Opts, current) of
current ->
is_current_major_version_at_least(VsnMin);
N when is_integer(N) ->
N >= VsnMin
end.
is_current_major_version_at_least(VsnMin) ->
current_otp_release() >= VsnMin.
current_otp_release() ->
case erlang:system_info(otp_release) of
"R"++Rest -> % R16 or ealier
FirstChunkOfDigits = lists:takewhile(fun is_digit/1, Rest),
list_to_integer(FirstChunkOfDigits);
RelStr ->
%% In Erlang 17 the leading "R" was dropped
%% The exact format isn't super documented,
%% so be prepared for some (future?) alternatives.
try list_to_integer(RelStr) of
N when is_integer(N) -> N
catch error:badarg ->
Rel = lists:dropwhile(fun is_not_digit/1, RelStr),
FirstChunkOfDigits = lists:takewhile(fun is_digit/1, Rel),
list_to_integer(FirstChunkOfDigits)
end
end.
is_not_digit(C) -> not is_digit(C).
is_digit(C) when $0 =< C, C =< $9 -> true;
is_digit(_) -> false.
%% Whether target version has the function lists:join/2.
target_has_lists_join(Opts) ->
is_target_major_version_at_least(19, Opts).
%% Whether target version supports M#{K => V} when K is a variable.
%% If before this support was added, one must use maps:put(K, V, M) instead.
target_has_variable_key_map_update(Opts) ->
is_target_major_version_at_least(18, Opts).
%% Whether target version supports #{key := type()} type spec syntax.
%% In Erlang 19, := indicates mandatory presence and => optional presence.
%% In Erlang 18, only => was supported.
target_can_specify_map_item_presence_in_typespecs(Opts) ->
is_target_major_version_at_least(19, Opts).
target_can_do_flat_oneof_for_maps(Opts) ->
%% Not possible in Erlang 17 because:
%% Variables as map keys appeared in 18.0. In 17, supports only literals
%% as map keys.
is_target_major_version_at_least(18, Opts).
target_may_fail_compilation_for_flat_oneof_for_maps(Opts) ->
%% In Erlang 18.3.4.6 .. 18.3.4.9
%% (ie the currently last/highest 4 Erlang 18 versions) this happens:
%% --
%% % erlc <erl for flat oneof>.erl
%% beamvalidatorerror: function v_msg_m1/3+75:
%% Internal consistency check failed - please report this bug.
%% Instruction: {move,{x,2},{y,0}}
%% Error: {uninitialized_reg,{x,2}}:
%% --
%% (introduced in c803276c9)
%% All Erlang 19 versions and later seems fine.
case target_can_do_flat_oneof_for_maps(Opts) of
true ->
AtLeast19 = is_target_major_version_at_least(19, Opts),
AtLeast18 = is_target_major_version_at_least(18, Opts),
AtLeast18 andalso (not AtLeast19);
false ->
true % On pre-18, it will definitely fail
end.
%% In Erlang 21, the function erlang:get_stacktrace/0 was deprecated
%% and there is new syntax for retrieving the stacktrace:
%%
%% try ...
%% catch Class:Reason:Stacktrace -> ...
%% end
target_has_stacktrace_syntax(Opts) ->
is_target_major_version_at_least(21, Opts).
proto2_type_default(Type, Defs, Opts) ->
type_default(Type, Defs, Opts, fun gpb:proto2_type_default/2).
proto3_type_default(Type, Defs, Opts) ->
type_default(Type, Defs, Opts, fun gpb:proto3_type_default/2).
type_default(Type, Defs, Opts, GetTypeDefault) ->
if Type == string ->
case get_strings_as_binaries_by_opts(Opts) of
true ->
list_to_binary(GetTypeDefault(Type, Defs));
false ->
GetTypeDefault(Type, Defs)
end;
Type /= string ->
GetTypeDefault(Type, Defs)
end.
get_maps_key_type_by_opts(Opts) ->
proplists:get_value(maps_key_type, Opts, atom).
%% Syntax tree stuff ----
var_f_n(N) -> var_n("F", N).
var_b_n(N) -> var_n("B", N).
var_n(S, N) ->
var("~s~w", [S, N]).
var(Fmt, Args) ->
erl_syntax:variable(?ff(Fmt, Args)).
prefix_var(Prefix, Var) ->
erl_syntax:variable(Prefix ++ erl_syntax:variable_literal(Var)).
match_bind_var(Pattern, Var) ->
?expr('Pattern' = 'Var',
[replace_tree('Pattern', Pattern),
replace_tree('Var', Var)]).
assign_to_var(Var, Expr) ->
?expr('<Var>' = '<Expr>',
[replace_tree('<Var>', Var),
replace_tree('<Expr>', Expr)]).
varint_to_binary_fields(IntValue) ->
[erl_syntax:binary_field(?expr('<n>', [replace_term('<n>', N)]), [])
|| N <- binary_to_list(gpb:encode_varint(IntValue))].
%% Given a sequence, `Seq', of expressions, and an initial expression,
%% Construct:
%% TmpVar1 = InitialExpr,
%% TmpVar2 = <1st expression in sequence, possibly involving TmpVar1>
%% TmpVar3 = <2st expression in sequence, possibly involving TmpVar2>
%% ...
%% <final expression in sequence, possibly involving TmpVarN-1>
do_exprs(F, InitExpr, Seq) ->
{LastExpr, ExprsReversed, _N} =
lists:foldl(
fun(Elem, {PrevE,Es,N}) ->
Var = var_n("S", N),
BoundPrevE = assign_to_var(Var, PrevE),
E = F(Elem, Var),
{E, [BoundPrevE | Es], N+1}
end,
{InitExpr, [], 1},
Seq),
lists:reverse([LastExpr | ExprsReversed]).
%% Misc ---
index_seq([]) -> [];
index_seq(L) -> lists:zip(lists:seq(1,length(L)), L).
smember(Elem, Set) -> %% set-member
sets:is_element(Elem, Set).
smember_any(Elems, Set) -> %% is any elem a member in the set
lists:any(fun(Elem) -> smember(Elem, Set) end,
Elems).
indent(Indent, Str) ->
lists:duplicate(Indent, $\s) ++ Str.
outdent_first(IoList) ->
lists:dropwhile(fun(C) -> C == $\s end,
binary_to_list(iolist_to_binary(IoList))).
indent_lines(Indent, Lines) ->
[indent(Indent, Line) || Line <- Lines].
split_indent_butfirst_iolist(Indent, IoList) ->
strip_left(iolist_to_binary(split_indent_iolist(Indent, IoList))).
strip_left(<<" ", Rest/binary>>) -> strip_left(Rest);
strip_left(Other) -> Other.
cond_split_indent_iolist(Condition, Indent, IoList) ->
B = iolist_to_binary(IoList),
case Condition(B) of
true -> split_indent_iolist(Indent, IoList);
false -> B
end.
split_indent_iolist(Indent, IoList) ->
[if Line == <<>> -> "\n"; %% don't indent empty lines
true -> [indent(Indent, Line), "\n"]
end
|| Line <- linesplit_iolist(IoList)].
linesplit_iolist(Iolist) ->
re:split(Iolist, ["\n"], [trim, {return,binary}]).
iolist_to_utf8_or_escaped_binary(IoList, Opts) ->
case understands_coding(Opts) of
true ->
unicode:characters_to_binary(
["%% -*- coding: utf-8 -*-\n",
IoList]);
false ->
%% What to do if on Erlang R15 or earlier? We can't utf8-encode
%% the file, because Erlang R15 will read it as latin1.
%%
%% For now, Assume such encodings are in strings only.
%% So far, this is safe, since neither message names nor field
%% names nor enum symbols are allowed to be non-ascii.
%%
%% This means only place for non-ascii is in comments and
%% in default strings. Hope I haven't overlooked some
%% important place...
iolist_to_binary(esc_non_ascii(IoList))
end.
understands_coding(Opts) ->
%% version coding: X default source encoding
%% R15: ignores latin1
%% R16: understands latin1
%% 17: understands utf-8
is_target_major_version_at_least(16, Opts).
esc_non_ascii([H|T]) -> [esc_non_ascii(H) | esc_non_ascii(T)];
esc_non_ascii([]) -> [];
esc_non_ascii(B) when is_binary(B) -> B;
esc_non_ascii(C) when is_integer(C), C =< 127 -> C;
esc_non_ascii(C) when is_integer(C), C > 127 -> ?f("\\x{~.16b}", [C]).
nowarn_dialyzer_attr(FnName,Arity,Opts) ->
%% Especially for the verifiers, dialyzer's success typing can
%% think that some code paths in the verifiers can't be reached,
%% and in a sense, it is right: the verifiers do much the same
%% work as dialyzer. But I think their existence is still
%% warranted because (a) they work-time rather than compile-time,
%% and (b) provide for shorter turn-around times when dialyzer
%% can take some time to analyze a non-trivial proto file.
%%
%% So mute dialyzer for the verifier functions.
case can_do_dialyzer_attr(Opts) of
true ->
?f("-dialyzer({nowarn_function,~p/~w}).~n", [FnName,Arity]);
false ->
%% Too old system (Erlang 17 or older), which will see
%% the dialyzer attr as just another plain attr,
%% which must be located before all functions.
%% Just don't silence dialyzer on these systems.
""
end.
can_do_dialyzer_attr(Opts) ->
is_target_major_version_at_least(18, Opts).
nowarn_unused_function(FnName, Arity) ->
?f("-compile({nowarn_unused_function,~p/~w}).~n", [FnName,Arity]).
-ifndef(NO_HAVE_ERL20_STR_FUNCTIONS).
comma_join(Elements) ->
lists:append(lists:join(", ", Elements)).
nl_join(Elements) ->
lists:append(lists:join("\n", Elements)).
or_join(Alternatives) ->
lists:append(lists:join(" | ", Alternatives)).
dot_join(Alternatives) ->
lists:append(lists:join(".", Alternatives)).
is_substr(SearchPattern, String) ->
string:find(String, SearchPattern) /= nomatch.
string_slice(String, Start) ->
string:slice(String, Start).
string_lexemes(String, Separators) ->
string:lexemes(String, Separators).
lowercase(Str) ->
string:lowercase(Str).
uppercase(Str) ->
string:uppercase(Str).
-else. % NO_HAVE_ERL20_STR_FUNCTIONS
comma_join(Elements) ->
string:join(Elements, ", ").
nl_join(Elements) ->
string:join(Elements, "\n").
or_join(Alternatives) ->
string:join(Alternatives, " | ").
dot_join(Alternatives) ->
string:join(Alternatives, ".").
is_substr(SearchPattern, String) ->
string:str(String, SearchPattern) > 0.
string_slice(String, Start0) ->
string:substr(String, Start0 + 1).
string_lexemes(String, Separators) ->
string:tokens(String, Separators).
lowercase(Str) ->
string:to_lower(Str).
uppercase(Str) ->
string:to_upper(Str).
-endif. % NO_HAVE_ERL20_STR_FUNCTIONS
snake_case(Str) ->
lowercase(
lists:foldl(fun(RE, Snaking) ->
re:replace(Snaking, RE, "\\1_\\2", [{return, list},
global])
end, Str, [%% uppercase followed by lowercase
"([^.])([A-Z][a-z]+)",
%% any consecutive digits
"([^.])([0-9]+)",
%% uppercase with lowercase
%% or digit before it
"([a-z0-9])([A-Z])"])).