Skip to content

Commit 109b1dc

Browse files
bjorngtomas-abrahamsson
authored andcommitted
epp: Implement the -if and -elif preprocessor directives
Libraries or applications that support more than one major release of OTP may need to use conditional compilation of Erlang source code. Here are few examples where it would be necessary or desirable: * To support a new data type or language feature only available in the latest major release (real-world examples: maps and the stacktrace syntax). * To avoid warnings for deprecated functions. * To avoid dialyzer warnings. Previously, to do conditional compilation, one would have to use a parse transform or some external tool such as 'autoconf'. To simplify conditional compilation, introduce the -if and -elif preprocessor directives, to allow code like this to be written: -if(?OTP_RELEASE =:= 21). %% Code that will only work in OTP 21. -else. %% Fallback code. -endif. What kind of expressions should be allowed after an -if? We certainly don't want to allow anything with a side effect, such as a '!' or a 'receive'. We also don't want it to be possible to call erlang:system_info/1, as that could make the code depedent on features of the run-time system that could change very easily (such as the number of schedulers). Requiring the expression to be a guard expression makes most sense. It is to explain in the documentation and easy for users to understand. For simplicity of implementation, only a single guard expression will be supported; that is, the ',' and ';' syntax for guards is not supported. To allow some useful conditions to be written, there is a special built-in function: defined(Symbol) tests whether the preprocessor symbol is defined, just like -ifdef. The reason for having this defined/1 is that the defined test can be combined with other tests, for example: 'defined(SOME_NAME) andalso ?OTP_RELEASE > 21'.
1 parent 55e8520 commit 109b1dc

2 files changed

Lines changed: 262 additions & 29 deletions

File tree

lib/stdlib/src/epp.erl

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
-type epp_handle() :: pid().
3939
-type source_encoding() :: latin1 | utf8.
4040

41-
-type ifdef() :: 'ifdef' | 'ifndef' | 'else'.
41+
-type ifdef() :: 'ifdef' | 'ifndef' | 'if' | 'else'.
4242

4343
-type name() :: atom().
4444
-type argspec() :: 'none' %No arguments
@@ -221,6 +221,8 @@ format_error({illegal_function,Macro}) ->
221221
io_lib:format("?~s can only be used within a function", [Macro]);
222222
format_error({illegal_function_usage,Macro}) ->
223223
io_lib:format("?~s must not begin a form", [Macro]);
224+
format_error(elif_after_else) ->
225+
"'elif' following 'else'";
224226
format_error({'NYI',What}) ->
225227
io_lib:format("not yet implemented '~s'", [What]);
226228
format_error({error,Term}) ->
@@ -1087,21 +1089,118 @@ scan_else(_Toks, Else, From, St) ->
10871089
epp_reply(From, {error,{loc(Else),epp,{bad,'else'}}}),
10881090
wait_req_scan(St).
10891091

1090-
%% scan_if(Tokens, EndifToken, From, EppState)
1092+
%% scan_if(Tokens, IfToken, From, EppState)
10911093
%% Handle the conditional parsing of a file.
1092-
%% Report a badly formed if test and then treat as false macro.
10931094

1095+
scan_if([{'(',_}|_]=Toks, If, From, St) ->
1096+
try eval_if(Toks, St) of
1097+
true ->
1098+
scan_toks(From, St#epp{istk=['if'|St#epp.istk]});
1099+
_ ->
1100+
skip_toks(From, St, ['if'])
1101+
catch
1102+
throw:Error0 ->
1103+
Error = case Error0 of
1104+
{_,erl_parse,_} ->
1105+
{error,Error0};
1106+
_ ->
1107+
{error,{loc(If),epp,Error0}}
1108+
end,
1109+
epp_reply(From, Error),
1110+
wait_req_skip(St, ['if'])
1111+
end;
10941112
scan_if(_Toks, If, From, St) ->
1095-
epp_reply(From, {error,{loc(If),epp,{'NYI','if'}}}),
1113+
epp_reply(From, {error,{loc(If),epp,{bad,'if'}}}),
10961114
wait_req_skip(St, ['if']).
10971115

1116+
eval_if(Toks0, St) ->
1117+
Toks = expand_macros(Toks0, St),
1118+
Es1 = case erl_parse:parse_exprs(Toks) of
1119+
{ok,Es0} -> Es0;
1120+
{error,E} -> throw(E)
1121+
end,
1122+
Es = rewrite_expr(Es1, St),
1123+
assert_guard_expr(Es),
1124+
Bs = erl_eval:new_bindings(),
1125+
LocalFun = fun(_Name, _Args) ->
1126+
error(badarg)
1127+
end,
1128+
try erl_eval:exprs(Es, Bs, {value,LocalFun}) of
1129+
{value,Res,_} ->
1130+
Res
1131+
catch
1132+
_:_ ->
1133+
false
1134+
end.
1135+
1136+
assert_guard_expr([E0]) ->
1137+
E = rewrite_expr(E0, none),
1138+
case erl_lint:is_guard_expr(E) of
1139+
false ->
1140+
throw({bad,'if'});
1141+
true ->
1142+
ok
1143+
end;
1144+
assert_guard_expr(_) ->
1145+
throw({bad,'if'}).
1146+
1147+
%% Dual-purpose rewriting function. When the second argument is
1148+
%% an #epp{} record, calls to defined(Symbol) will be evaluated.
1149+
%% When the second argument is 'none', legal calls to our built-in
1150+
%% functions are eliminated in order to turn the expression into
1151+
%% a legal guard expression.
1152+
1153+
rewrite_expr({call,_,{atom,_,defined},[N0]}, #epp{macs=Macs}) ->
1154+
%% Evaluate defined(Symbol).
1155+
N = case N0 of
1156+
{var,_,N1} -> N1;
1157+
{atom,_,N1} -> N1;
1158+
_ -> throw({bad,'if'})
1159+
end,
1160+
{atom,0,maps:is_key(N, Macs)};
1161+
rewrite_expr({call,_,{atom,_,Name},As0}, none) ->
1162+
As = rewrite_expr(As0, none),
1163+
Arity = length(As),
1164+
case erl_internal:bif(Name, Arity) andalso
1165+
not erl_internal:guard_bif(Name, Arity) of
1166+
false ->
1167+
%% A guard BIF, an -if built-in, or an unknown function.
1168+
%% Eliminate the call so that erl_lint will not complain.
1169+
%% The call might fail later at evaluation time.
1170+
to_conses(As);
1171+
true ->
1172+
%% An auto-imported BIF (not guard BIF). Not allowed.
1173+
throw({bad,'if'})
1174+
end;
1175+
rewrite_expr([H|T], St) ->
1176+
[rewrite_expr(H, St)|rewrite_expr(T, St)];
1177+
rewrite_expr(Tuple, St) when is_tuple(Tuple) ->
1178+
list_to_tuple(rewrite_expr(tuple_to_list(Tuple), St));
1179+
rewrite_expr(Other, _) ->
1180+
Other.
1181+
1182+
to_conses([H|T]) ->
1183+
{cons,0,H,to_conses(T)};
1184+
to_conses([]) ->
1185+
{nil,0}.
1186+
10981187
%% scan_elif(Tokens, EndifToken, From, EppState)
10991188
%% Handle the conditional parsing of a file.
11001189
%% Report a badly formed if test and then treat as false macro.
11011190

11021191
scan_elif(_Toks, Elif, From, St) ->
1103-
epp_reply(From, {error,{loc(Elif),epp,{'NYI','elif'}}}),
1104-
wait_req_scan(St).
1192+
case St#epp.istk of
1193+
['else'|Cis] ->
1194+
epp_reply(From, {error,{loc(Elif),
1195+
epp,{illegal,"unbalanced",'elif'}}}),
1196+
wait_req_skip(St#epp{istk=Cis}, ['else']);
1197+
[_I|Cis] ->
1198+
skip_toks(From, St#epp{istk=Cis}, ['elif']);
1199+
[] ->
1200+
epp_reply(From, {error,{loc(Elif),epp,
1201+
{illegal,"unbalanced",elif}}}),
1202+
wait_req_scan(St)
1203+
end.
11051204

11061205
%% scan_endif(Tokens, EndifToken, From, EppState)
11071206
%% If we are in an if body then exit it, else report an error.
@@ -1160,6 +1259,8 @@ skip_toks(From, St, [I|Sis]) ->
11601259
skip_toks(From, St#epp{location=Cl}, ['if',I|Sis]);
11611260
{ok,[{'-',_Lh},{atom,_Le,'else'}=Else|_Toks],Cl}->
11621261
skip_else(Else, From, St#epp{location=Cl}, [I|Sis]);
1262+
{ok,[{'-',_Lh},{atom,_Le,'elif'}=Elif|Toks],Cl}->
1263+
skip_elif(Toks, Elif, From, St#epp{location=Cl}, [I|Sis]);
11631264
{ok,[{'-',_Lh},{atom,_Le,endif}|_Toks],Cl} ->
11641265
skip_toks(From, St#epp{location=Cl}, Sis);
11651266
{ok,_Toks,Cl} ->
@@ -1190,11 +1291,21 @@ skip_toks(From, St, []) ->
11901291
skip_else(Else, From, St, ['else'|Sis]) ->
11911292
epp_reply(From, {error,{loc(Else),epp,{illegal,"repeated",'else'}}}),
11921293
wait_req_skip(St, ['else'|Sis]);
1294+
skip_else(_Else, From, St, ['elif'|Sis]) ->
1295+
skip_toks(From, St, ['else'|Sis]);
11931296
skip_else(_Else, From, St, [_I]) ->
11941297
scan_toks(From, St#epp{istk=['else'|St#epp.istk]});
11951298
skip_else(_Else, From, St, Sis) ->
11961299
skip_toks(From, St, Sis).
11971300

1301+
skip_elif(_Toks, Elif, From, St, ['else'|_]=Sis) ->
1302+
epp_reply(From, {error,{loc(Elif),epp,elif_after_else}}),
1303+
wait_req_skip(St, Sis);
1304+
skip_elif(Toks, Elif, From, St, [_I]) ->
1305+
scan_if(Toks, Elif, From, St);
1306+
skip_elif(_Toks, _Elif, From, St, Sis) ->
1307+
skip_toks(From, St, Sis).
1308+
11981309
%% macro_pars(Tokens, ArgStack)
11991310
%% macro_expansion(Tokens, Anno)
12001311
%% Extract the macro parameters and the expansion from a macro definition.

lib/stdlib/test/epp_SUITE.erl

Lines changed: 145 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
otp_8130/1, overload_mac/1, otp_8388/1, otp_8470/1,
2929
otp_8562/1, otp_8665/1, otp_8911/1, otp_10302/1, otp_10820/1,
3030
otp_11728/1, encoding/1, extends/1, function_macro/1,
31-
test_error/1, test_warning/1, otp_14285/1]).
31+
test_error/1, test_warning/1, otp_14285/1,
32+
test_if/1]).
3233

3334
-export([epp_parse_erl_form/2]).
3435

@@ -69,7 +70,7 @@ all() ->
6970
overload_mac, otp_8388, otp_8470, otp_8562,
7071
otp_8665, otp_8911, otp_10302, otp_10820, otp_11728,
7172
encoding, extends, function_macro, test_error, test_warning,
72-
otp_14285].
73+
otp_14285, test_if].
7374

7475
groups() ->
7576
[{upcase_mac, [], [upcase_mac_1, upcase_mac_2]},
@@ -953,27 +954,7 @@ ifdef(Config) ->
953954

954955
{define_c5,
955956
<<"-\ndefine a.\n">>,
956-
{errors,[{{2,1},epp,{bad,define}}],[]}},
957-
958-
{define_c6,
959-
<<"\n-if.\n"
960-
"-endif.\n">>,
961-
{errors,[{{2,2},epp,{'NYI','if'}}],[]}},
962-
963-
{define_c7,
964-
<<"-ifndef(a).\n"
965-
"-elif.\n"
966-
"-endif.\n">>,
967-
{errors,[{{2,2},epp,{'NYI',elif}}],[]}},
968-
969-
{define_c7,
970-
<<"-ifndef(a).\n"
971-
"-if.\n"
972-
"-elif.\n"
973-
"-endif.\n"
974-
"-endif.\n"
975-
"t() -> a.\n">>,
976-
{errors,[{{2,2},epp,{'NYI','if'}}],[]}}
957+
{errors,[{{2,1},epp,{bad,define}}],[]}}
977958
],
978959
[] = compile(Config, Cs),
979960

@@ -1118,6 +1099,147 @@ test_warning(Config) ->
11181099
[] = compile(Config, Cs),
11191100
ok.
11201101

1102+
%% OTP-12847: Test the -if and -elif directives and the built-in
1103+
%% function defined(Symbol).
1104+
test_if(Config) ->
1105+
Cs = [{if_1c,
1106+
<<"-if.\n"
1107+
"-endif.\n"
1108+
"-if no_parentheses.\n"
1109+
"-endif.\n"
1110+
"-if(syntax error.\n"
1111+
"-endif.\n"
1112+
"-if(true).\n"
1113+
"-if(a+3).\n"
1114+
"syntax error not triggered here.\n"
1115+
"-endif.\n">>,
1116+
{errors,[{1,epp,{bad,'if'}},
1117+
{3,epp,{bad,'if'}},
1118+
{5,erl_parse,["syntax error before: ","error"]},
1119+
{11,epp,{illegal,"unterminated",'if'}}],
1120+
[]}},
1121+
1122+
{if_2c, %Bad guard expressions.
1123+
<<"-if(is_list(integer_to_list(42))).\n" %Not guard BIF.
1124+
"-endif.\n"
1125+
"-if(begin true end).\n"
1126+
"-endif.\n">>,
1127+
{errors,[{1,epp,{bad,'if'}},
1128+
{3,epp,{bad,'if'}}],
1129+
[]}},
1130+
1131+
{if_3c, %Invalid use of defined/1.
1132+
<<"-if defined(42).\n"
1133+
"-endif.\n">>,
1134+
{errors,[{1,epp,{bad,'if'}}],[]}},
1135+
1136+
{if_4c,
1137+
<<"-elif OTP_RELEASE > 18.\n">>,
1138+
{errors,[{1,epp,{illegal,"unbalanced",'elif'}}],[]}},
1139+
1140+
{if_5c,
1141+
<<"-ifdef(not_defined_today).\n"
1142+
"-else.\n"
1143+
"-elif OTP_RELEASE > 18.\n"
1144+
"-endif.\n">>,
1145+
{errors,[{3,epp,{illegal,"unbalanced",'elif'}}],[]}},
1146+
1147+
{if_6c,
1148+
<<"-if(defined(OTP_RELEASE)).\n"
1149+
"-else.\n"
1150+
"-elif(true).\n"
1151+
"-endif.\n">>,
1152+
{errors,[{3,epp,elif_after_else}],[]}},
1153+
1154+
{if_7c,
1155+
<<"-if(begin true end).\n" %Not a guard expression.
1156+
"-endif.\n">>,
1157+
{errors,[{1,epp,{bad,'if'}}],[]}}
1158+
1159+
],
1160+
[] = compile(Config, Cs),
1161+
1162+
Ts = [{if_1,
1163+
<<"-if(?OTP_RELEASE > 18).\n"
1164+
"t() -> ok.\n"
1165+
"-else.\n"
1166+
"a bug.\n"
1167+
"-endif.\n">>,
1168+
ok},
1169+
1170+
{if_2,
1171+
<<"-if(false).\n"
1172+
"a bug.\n"
1173+
"-elif(?OTP_RELEASE > 18).\n"
1174+
"t() -> ok.\n"
1175+
"-else.\n"
1176+
"a bug.\n"
1177+
"-endif.\n">>,
1178+
ok},
1179+
1180+
{if_3,
1181+
<<"-if(true).\n"
1182+
"t() -> ok.\n"
1183+
"-elif(?OTP_RELEASE > 18).\n"
1184+
"a bug.\n"
1185+
"-else.\n"
1186+
"a bug.\n"
1187+
"-endif.\n">>,
1188+
ok},
1189+
1190+
{if_4,
1191+
<<"-define(a, 1).\n"
1192+
"-if(defined(a) andalso defined(OTP_RELEASE)).\n"
1193+
"t() -> ok.\n"
1194+
"-else.\n"
1195+
"a bug.\n"
1196+
"-endif.\n">>,
1197+
ok},
1198+
1199+
{if_5,
1200+
<<"-if(defined(a)).\n"
1201+
"a bug.\n"
1202+
"-else.\n"
1203+
"t() -> ok.\n"
1204+
"-endif.\n">>,
1205+
ok},
1206+
1207+
{if_6,
1208+
<<"-if(defined(not_defined_today)).\n"
1209+
" -if(true).\n"
1210+
" bug1.\n"
1211+
" -elif(true).\n"
1212+
" bug2.\n"
1213+
" -elif(true).\n"
1214+
" bug3.\n"
1215+
" -else.\n"
1216+
" bug4.\n"
1217+
" -endif.\n"
1218+
"-else.\n"
1219+
"t() -> ok.\n"
1220+
"-endif.\n">>,
1221+
ok},
1222+
1223+
{if_7,
1224+
<<"-if(not_builtin()).\n"
1225+
"a bug.\n"
1226+
"-else.\n"
1227+
"t() -> ok.\n"
1228+
"-endif.\n">>,
1229+
ok},
1230+
1231+
{if_8,
1232+
<<"-if(42).\n" %Not boolean.
1233+
"a bug.\n"
1234+
"-else.\n"
1235+
"t() -> ok.\n"
1236+
"-endif.\n">>,
1237+
ok}
1238+
],
1239+
[] = run(Config, Ts),
1240+
1241+
ok.
1242+
11211243
%% Advanced test on overloading macros.
11221244
overload_mac(Config) when is_list(Config) ->
11231245
Cs = [

0 commit comments

Comments
 (0)