Skip to content

Commit cbb87a5

Browse files
committed
Cache results across
1 parent b5f5e04 commit cbb87a5

3 files changed

Lines changed: 169 additions & 136 deletions

File tree

lib/elixir/lib/module/types.ex

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,19 @@ defmodule Module.Types do
468468
end
469469

470470
defp fresh_stack(stack, mode, function) when mode in @modes do
471-
%{stack | mode: mode, function: function}
471+
%{stack | mode: mode, function: function, reverse_arrow: nil}
472472
end
473473

474474
defp fresh_context(context) do
475-
%{context | vars: %{}, failed: false}
475+
%{context | vars: %{}, failed: false, reverse_arrows: %{}}
476476
end
477477

478-
defp restore_context(later_context, %{vars: vars, failed: failed}) do
479-
%{later_context | vars: vars, failed: failed}
478+
defp restore_context(later_context, %{
479+
vars: vars,
480+
failed: failed,
481+
reverse_arrows: reverse_arrows
482+
}) do
483+
%{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows}
480484
end
481485

482486
## Diagnostics

lib/elixir/lib/module/types/expr.ex

Lines changed: 160 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -287,34 +287,36 @@ defmodule Module.Types.Expr do
287287
of_expr(post, expected, post, stack, context)
288288
end
289289

290-
def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected, expr, stack, original) do
291-
clauses
292-
|> reduce_non_empty({none(), original}, fn
293-
{:->, meta, [[head], body]}, {acc, context}, last? ->
294-
{head_type, context} = of_expr(head, @pending, head, stack, context)
295-
296-
context =
297-
if is_warning(stack) do
298-
case truthiness(head_type) do
299-
:always_true when not last? ->
300-
warning = {:badcond, "always match", head_type, head, context}
301-
warn(__MODULE__, warning, meta, stack, context)
302-
303-
:always_false ->
304-
warning = {:badcond, "never match", head_type, head, context}
305-
warn(__MODULE__, warning, meta, stack, context)
306-
307-
_ ->
308-
context
290+
def of_expr({:cond, meta, [[{:do, clauses}]]}, expected, expr, stack, original) do
291+
cache_result(meta, stack, original, fn ->
292+
clauses
293+
|> reduce_non_empty({none(), original}, fn
294+
{:->, meta, [[head], body]}, {acc, context}, last? ->
295+
{head_type, context} = of_expr(head, @pending, head, stack, context)
296+
297+
context =
298+
if is_warning(stack) do
299+
case truthiness(head_type) do
300+
:always_true when not last? ->
301+
warning = {:badcond, "always match", head_type, head, context}
302+
warn(__MODULE__, warning, meta, stack, context)
303+
304+
:always_false ->
305+
warning = {:badcond, "never match", head_type, head, context}
306+
warn(__MODULE__, warning, meta, stack, context)
307+
308+
_ ->
309+
context
310+
end
311+
else
312+
context
309313
end
310-
else
311-
context
312-
end
313314

314-
{body_type, context} = of_expr(body, expected, expr, stack, context)
315-
{union(body_type, acc), Of.reset_vars(context, original)}
315+
{body_type, context} = of_expr(body, expected, expr, stack, context)
316+
{union(body_type, acc), Of.reset_vars(context, original)}
317+
end)
318+
|> dynamic_unless_static(stack)
316319
end)
317-
|> dynamic_unless_static(stack)
318320
end
319321

320322
def of_expr({:case, meta, [case_expr, [{:do, _clauses}]]}, expected, _expr, stack, context)
@@ -383,139 +385,149 @@ defmodule Module.Types.Expr do
383385
end
384386

385387
# fn pat -> expr end
386-
def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do
387-
[{:->, _, [head, _]} | _] = clauses
388-
{patterns, _guards} = extract_head(head)
389-
domain = Enum.map(patterns, fn _ -> dynamic() end)
390-
391-
of_body = fn _args_types, body, context -> of_expr(body, term(), body, stack, context) end
392-
393-
{acc, context} =
394-
of_clauses_fun(clauses, domain, :fn, stack, context, of_body, [], fn
395-
trees, body_type, context, acc ->
396-
args_types = Pattern.of_domain(trees, stack, context)
397-
add_inferred(acc, args_types, body_type)
398-
end)
388+
def of_expr({:fn, meta, clauses}, _expected, _expr, stack, context) do
389+
cache_result(meta, stack, context, fn ->
390+
[{:->, _, [head, _]} | _] = clauses
391+
{patterns, _guards} = extract_head(head)
392+
domain = Enum.map(patterns, fn _ -> dynamic() end)
399393

400-
{fun_from_inferred_clauses(acc), context}
394+
of_body = fn _args_types, body, context -> of_expr(body, term(), body, stack, context) end
395+
396+
{acc, context} =
397+
of_clauses_fun(clauses, domain, :fn, stack, context, of_body, [], fn
398+
trees, body_type, context, acc ->
399+
args_types = Pattern.of_domain(trees, stack, context)
400+
add_inferred(acc, args_types, body_type)
401+
end)
402+
403+
{fun_from_inferred_clauses(acc), context}
404+
end)
401405
end
402406

403407
def of_expr({:try, meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
404-
{after_block, blocks} = Keyword.pop(blocks, :after)
405-
{else_block, blocks} = Keyword.pop(blocks, :else)
406-
407-
{type, context} =
408-
if else_block do
409-
{type, context} = of_expr(body, @pending, body, stack, original)
410-
info = {:try_else, meta, body, type}
411-
of_clauses(else_block, [type], expected, info, stack, context, none())
412-
else
413-
of_expr(body, expected, expr, stack, original)
414-
end
408+
cache_result(meta, stack, original, fn ->
409+
{after_block, blocks} = Keyword.pop(blocks, :after)
410+
{else_block, blocks} = Keyword.pop(blocks, :else)
411+
412+
{type, context} =
413+
if else_block do
414+
{type, context} = of_expr(body, @pending, body, stack, original)
415+
info = {:try_else, meta, body, type}
416+
of_clauses(else_block, [type], expected, info, stack, context, none())
417+
else
418+
of_expr(body, expected, expr, stack, original)
419+
end
415420

416-
{type, context} =
417-
blocks
418-
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
419-
{:rescue, clauses}, acc_context ->
420-
Enum.reduce(clauses, acc_context, fn
421-
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
422-
{type, context} =
423-
of_rescue(var, exceptions, body, expr, :rescue, meta, stack, context)
421+
{type, context} =
422+
blocks
423+
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
424+
{:rescue, clauses}, acc_context ->
425+
Enum.reduce(clauses, acc_context, fn
426+
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
427+
{type, context} =
428+
of_rescue(var, exceptions, body, expr, :rescue, meta, stack, context)
424429

425-
{union(type, acc), context}
430+
{union(type, acc), context}
426431

427-
{:->, meta, [[var], body]}, {acc, context} ->
428-
{type, context} =
429-
of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context)
432+
{:->, meta, [[var], body]}, {acc, context} ->
433+
{type, context} =
434+
of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context)
430435

431-
{union(type, acc), context}
432-
end)
436+
{union(type, acc), context}
437+
end)
433438

434-
{:catch, clauses}, {acc, context} ->
435-
args = [@try_catch, dynamic()]
436-
of_clauses(clauses, args, expected, :try_catch, stack, context, acc)
437-
end)
438-
|> dynamic_unless_static(stack)
439+
{:catch, clauses}, {acc, context} ->
440+
args = [@try_catch, dynamic()]
441+
of_clauses(clauses, args, expected, :try_catch, stack, context, acc)
442+
end)
443+
|> dynamic_unless_static(stack)
439444

440-
if after_block do
441-
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
442-
{type, context}
443-
else
444-
{type, context}
445-
end
445+
if after_block do
446+
{_type, context} = of_expr(after_block, term(), after_block, stack, context)
447+
{type, context}
448+
else
449+
{type, context}
450+
end
451+
end)
446452
end
447453

448454
@timeout_type union(integer(), atom([:infinity]))
449455

450-
def of_expr({:receive, _meta, [blocks]}, expected, expr, stack, original) do
451-
blocks
452-
|> Enum.reduce({none(), original}, fn
453-
{:do, {:__block__, _, []}}, acc_context ->
454-
acc_context
456+
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
457+
cache_result(meta, stack, original, fn ->
458+
blocks
459+
|> Enum.reduce({none(), original}, fn
460+
{:do, {:__block__, _, []}}, acc_context ->
461+
acc_context
455462

456-
{:do, clauses}, {acc, context} ->
457-
of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc)
463+
{:do, clauses}, {acc, context} ->
464+
of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc)
458465

459-
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
460-
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
461-
{body_type, context} = of_expr(body, expected, expr, stack, context)
466+
{:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} ->
467+
{timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context)
468+
{body_type, context} = of_expr(body, expected, expr, stack, context)
462469

463-
if compatible?(timeout_type, @timeout_type) do
464-
{union(body_type, acc), Of.reset_vars(context, original)}
465-
else
466-
error = {:badtimeout, timeout_type, timeout, context}
467-
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
468-
end
470+
if compatible?(timeout_type, @timeout_type) do
471+
{union(body_type, acc), Of.reset_vars(context, original)}
472+
else
473+
error = {:badtimeout, timeout_type, timeout, context}
474+
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
475+
end
476+
end)
477+
|> dynamic_unless_static(stack)
469478
end)
470-
|> dynamic_unless_static(stack)
471479
end
472480

473481
def of_expr({:for, meta, [_ | _] = args}, expected, expr, stack, context) do
474-
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
475-
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
476-
477-
# We don't need to type check uniq, as it is a compile-time boolean.
478-
# We handle reduce and into accordingly instead.
479-
if Keyword.has_key?(opts, :reduce) do
480-
reduce = Keyword.fetch!(opts, :reduce)
481-
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
482-
# TODO: We need to type check against dynamic() instead of using reduce_type
483-
# because this is recursive. We need to infer the block type first.
484-
args = [dynamic()]
485-
of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type)
486-
else
487-
# TODO: Use the collectable protocol for the output
488-
# TODO: Use the expected type for the block output
489-
into = Keyword.get(opts, :into, [])
490-
{into_type, into_kind, context} = for_into(into, meta, stack, context)
491-
{block_type, context} = of_expr(block, @pending, block, stack, context)
492-
493-
case into_kind do
494-
:bitstring ->
495-
case compatible_intersection(block_type, bitstring()) do
496-
{:ok, intersection} ->
497-
{return_union(into_type, intersection, stack), context}
498-
499-
{:error, _} ->
500-
error = {:badbitbody, block_type, block, context}
501-
{error_type(), error(__MODULE__, error, meta, stack, context)}
502-
end
482+
cache_result(meta, stack, context, fn ->
483+
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
484+
context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2))
485+
486+
# We don't need to type check uniq, as it is a compile-time boolean.
487+
# We handle reduce and into accordingly instead.
488+
if Keyword.has_key?(opts, :reduce) do
489+
reduce = Keyword.fetch!(opts, :reduce)
490+
{reduce_type, context} = of_expr(reduce, expected, expr, stack, context)
491+
# TODO: We need to type check against dynamic() instead of using reduce_type
492+
# because this is recursive. We need to infer the block type first.
493+
args = [dynamic()]
494+
of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type)
495+
else
496+
# TODO: Use the collectable protocol for the output
497+
# TODO: Use the expected type for the block output
498+
into = Keyword.get(opts, :into, [])
499+
{into_type, into_kind, context} = for_into(into, meta, stack, context)
500+
{block_type, context} = of_expr(block, @pending, block, stack, context)
501+
502+
case into_kind do
503+
:bitstring ->
504+
case compatible_intersection(block_type, bitstring()) do
505+
{:ok, intersection} ->
506+
{return_union(into_type, intersection, stack), context}
507+
508+
{:error, _} ->
509+
error = {:badbitbody, block_type, block, context}
510+
{error_type(), error(__MODULE__, error, meta, stack, context)}
511+
end
503512

504-
:non_empty_list ->
505-
{return_union(into_type, non_empty_list(block_type), stack), context}
513+
:non_empty_list ->
514+
{return_union(into_type, non_empty_list(block_type), stack), context}
506515

507-
:none ->
508-
{into_type, context}
516+
:none ->
517+
{into_type, context}
518+
end
509519
end
510-
end
520+
end)
511521
end
512522

513523
# TODO: with pat <- expr do expr end
514-
def of_expr({:with, _meta, [_ | _] = clauses}, _expected, _expr, stack, original) do
515-
{clauses, [options]} = Enum.split(clauses, -1)
516-
context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2))
517-
context = Enum.reduce(options, context, &with_option(&1, stack, &2, original))
518-
{dynamic(), context}
524+
def of_expr({:with, meta, [_ | _] = clauses}, _expected, _expr, stack, original) do
525+
cache_result(meta, stack, original, fn ->
526+
{clauses, [options]} = Enum.split(clauses, -1)
527+
context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2))
528+
context = Enum.reduce(options, context, &with_option(&1, stack, &2, original))
529+
{dynamic(), context}
530+
end)
519531
end
520532

521533
def of_expr({{:., _, [fun]}, _, args} = call, _expected, _expr, stack, context) do
@@ -775,6 +787,23 @@ defmodule Module.Types.Expr do
775787
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
776788
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
777789

790+
defp cache_result(meta, %{reverse_arrow: reverse_arrow}, context, fun) do
791+
case reverse_arrow do
792+
nil ->
793+
fun.()
794+
795+
:cache ->
796+
{result, context} = fun.()
797+
version = Keyword.fetch!(meta, :version)
798+
context = put_in(context.reverse_arrows[version], result)
799+
{result, context}
800+
801+
:use ->
802+
version = Keyword.fetch!(meta, :version)
803+
{Map.fetch!(context.reverse_arrows, version), context}
804+
end
805+
end
806+
778807
defp cache_arrows(_meta, %{reverse_arrow: nil}, _fun), do: nil
779808

780809
defp cache_arrows(meta, %{reverse_arrow: :cache}, fun) do

lib/elixir/src/elixir_clauses.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ with(Meta, Args, S, E) ->
322322
ok
323323
end,
324324

325-
#elixir_ex{version=Counter} = S3,
325+
#elixir_ex{version=Counter} = S3,
326326
{{with, [{version, Counter} | Meta], EExprs ++ [[{do, EDo} | EOpts]]},
327327
S3#elixir_ex{version=Counter+1}, E}.
328328

0 commit comments

Comments
 (0)