-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Temporary measure: break cycles by timing out #383
Conversation
I have taken another look at it and I have to correct the above. I couldn't find evidence about the current master branch looping on real world code. Only on synthetic, generated recursive types in property tests. Below is a comparison between 3 branches:
The test file contains 3 problems I identified when dogfooding with Gradualizer as a background diagnostics provider for ErlangLS or by running
The results are that:
I also did a similar test of the code from #360 - the results are here: https://gist.github.com/erszcz/4323e97860e67da290f52e2b0998a863 (search for timeout). The only timing out function here is The test file in full detail: -module(syntax).
f( Needle, [Needle | _]) -> ok;
f( Needle, [_ | Haystack]) -> f(Needle, Haystack);
f(_Needle, []) -> ok.
-spec g([a | b]) -> ok.
g([a | _Haystack]) -> ok;
g([_ | Haystack]) -> g(Haystack);
g([]) -> ok.
-spec h([a | b]) -> ok.
h([a | _Haystack]) -> ok;
h([]) -> ok;
h([_ | Haystack]) -> h(Haystack).
%%
%%' Datatypes
%%
-type ty() :: {var, index(), context_size()}
| {id, string()}
| {arr, ty(), ty()}
| unit
| {record, [{string(), ty()}]}
| {rec, string(), ty()}
| {variant, [{string(), ty()}]}
| bool
| string
| float
| nat.
-type info() :: {pos_integer(), pos_integer()}.
-type token() :: {atom(), info()}
| {atom(), info(), string()}.
-type index() :: non_neg_integer().
-type context_size() :: non_neg_integer().
-type term_() :: {true, info()}
| {false, info()}
| {if_, info(), term_(), term_(), term_()}
| {case_, info(), term_(), [{string(), {string(), term_()}}]}
| {tag, info(), string(), term_(), ty()}
| {var, info(), index(), context_size()}
| {abs, info(), string(), ty(), term_()}
| {app, info(), term_(), term_()}
| {let_, info(), string(), term_(), term_()}
| {fix, info(), term_()}
| {string, info(), string()}
| {unit, info()}
| {ascribe, info(), term_(), ty()}
| {proj, info(), term_(), string()}
| {record, info(), [{string(), term_()}]}
| {float, info(), float()}
| {timesfloat, info(), term_(), term_()}
| {zero, info()}
| {succ, info(), term_()}
| {pred, info(), term_()}
| {is_zero, info(), term_()}
| {inert, info(), ty()}.
%% TAPL `term' type, but `term()' is a builtin type in Erlang,
%% hence the name `term_()'.
-type binding() :: name_bind
| ty_var_bind
| {var_bind, ty()}
| {tm_abb_bind, term_(), ty() | none}
| {ty_abb_bind, ty()}.
-type context() :: [{string(), binding()}].
-type command() :: {eval, info(), term()}
| {bind, info(), string(), binding()}.
%%.
%%' Constructors
%%
-spec ty(ty()) -> ty().
ty(Ty) ->
case Ty of
{var, _, _} -> Ty;
%{id, _} -> Ty;
{arr, _, _} -> Ty;
unit -> Ty;
{record, _} -> Ty;
{rec, _, _} -> Ty;
{variant, _} -> Ty;
bool -> Ty;
string -> Ty;
float -> Ty;
nat -> Ty
end.
-spec info(token()) -> info().
info({_, Info}) -> Info;
info({_, Info, _Chars}) -> Info.
%% This function might seem useless, since it "doesn't do anything",
%% but in fact it gives us two guarantees:
%% - thanks to Gradualizer it provides compile-time warnings if we build an invalid term_()
%% - it fails fast (aka crashes) at runtime if we try to build an invalid term_()
-spec term_(term_()) -> term_().
term_(T) ->
case T of
{true, _} -> T;
%{false, _} -> T;
{if_, _, _, _, _} -> T;
{case_, _, _, _} -> T;
{tag, _, _, _, _} -> T;
{var, _, _, _} -> T;
{abs, _, _, _, _} -> T;
{app, _, _, _} -> T;
{let_, _, _, _, _} -> T;
{fix, _, _} -> T;
{string, _, _} -> T;
{unit, _} -> T;
{ascribe, _, _, _} -> T;
{proj, _, _, _} -> T;
{record, _, _} -> T;
{float, _, _} -> T;
{timesfloat, _, _, _} -> T;
{zero, _} -> T;
{succ, _, _} -> T;
{pred, _, _} -> T;
{is_zero, _, _} -> T;
{inert, _, _} -> T
end.
-spec binding(binding()) -> binding().
binding(B) ->
case B of
name_bind -> B;
ty_var_bind -> B;
{var_bind, _} -> B;
{tm_abb_bind, _, _} -> B;
{ty_abb_bind, _} -> B
end.
-spec command(command()) -> command().
command(C) ->
case C of
{eval, _, _} -> C;
{bind, _, _, _} -> C
end.
%%.
-spec format_binding_type(context(), binding()) -> io_lib:chars().
%-spec format_binding_type(context(), binding()) -> string().
format_binding_type(Ctx, B) ->
case B of
name_bind -> "";
ty_var_bind -> "";
{var_bind, TyT} ->
io_lib:format(": ~ts", [zxcqwe]);
{tm_abb_bind, T, MaybeTyT} ->
case MaybeTyT of
none -> [": ", "asdzxc"];
TyT -> [": ", "qwe123"]
end;
{ty_abb_bind, _} -> ":: *"
end. Results:
|
src/gradualizer_task.erl
Outdated
%% This is a port of the Elixir Task module borrowed from https://github.com/redink/task. | ||
%% Original Elixir Task documentation can be found at https://hexdocs.pm/elixir/Task.html. | ||
%% @end | ||
-module(gradualizer_task). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this would be part of the standard library (as it is in Elixir) I'd be fine using it. Porting it and including it here seems like lots of added complexity though. Isn't it enough to use spawn_link or spawn_monitor and set a timeout?
%% Pseudo-code
TypecheckerPid = spawn_link(fun () -> type_check(Froms, self()) end),
receive
{typechecker_done, Result} -> Result;
after Timeout ->
kill(TypecheckerPid),
{timeout, Forms)
end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It worked pretty well for me, but we now have a hand rolled timeout/3
if you prefer 🤷♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zuiderkwast you must've got some sixth sense 😆 It turned out the task.erl
would return with a timeout, so other forms would be checked and reported, but at the same time the async task would go rogue and hog the CPU in an infinite loop anyway. I've now tested in ErlangLS with the hand-rolled spawn_monitor
and timeout and the CPU is way less busy 👍
5cb093f
to
7be591d
Compare
7be591d
to
b8db4d6
Compare
@zuiderkwast This is ready with the fix you suggested. Do you have any further comments? |
src/gradualizer_fmt.erl
Outdated
@@ -342,6 +346,20 @@ format_type_error({bad_type_annotation, TypeLit}, Opts) -> | |||
[format_location(TypeLit, brief, Opts), | |||
pp_expr(TypeLit, Opts), | |||
format_location(TypeLit, verbose, Opts)]); | |||
format_type_error({internal_error, form_check_timeout, Form}, Opts) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think {timeout, Form}
would be enough, to keep the error format simpler.
Co-authored-by: Viktor Söderqvist <viktor@zuiderkwast.se>
c5a636d
to
235965d
Compare
Current master branch is known to fall into infinite loops on some forms. It's evidenced by the property tests and real-world issues like #360.EDIT: please see #383 (comment) for the explanation why this is crossed out.This PR tries to improve the UX and facilitate dogfooding by a temporary measure of forcibly breaking infinite loops after a timeout and therefore turning situations of hogging the CPU or never terminating into warning messages.
When Gradualizer is run interactively from the CLI or as a Rebar plugin, it's easy to conclude that it's stuck on a given file. However, it's still problematic when it gets stuck, as it prevents us from assessing the number of forms in the project which cause it to loop.
When Gradualizer is run as a background integration task, the only means of realising that it's stuck is the lack of diagnostics and the sound of fans cooling down the CPU.
This PR improves the situation in both of the above cases:
Timeout
warnings to understand how many forms causing Gradualizer to fail are present in real-world code,Timeout
diagnostic, providing valuable feedback on other forms which do not time out and are successfully type checked.