Skip to content

Commit 11ab91c

Browse files
author
José Valim
committed
Merge pull request #1928 from ericmj/typespec-kw-guard
Change typespec guards to keyword list
2 parents 91dc65b + 9bedb0f commit 11ab91c

File tree

6 files changed

+174
-86
lines changed

6 files changed

+174
-86
lines changed

lib/elixir/lib/kernel/typespec.ex

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -243,24 +243,41 @@ defmodule Kernel.Typespec do
243243
Converts a spec clause back to Elixir AST.
244244
"""
245245
def spec_to_ast(name, { :type, line, :fun, [{:type, _, :product, args}, result] }) do
246-
args = lc arg inlist args, do: typespec_to_ast(arg)
247-
{ :::, [line: line], [{ name, [line: line], args }, typespec_to_ast(result)] }
246+
ast_args = lc arg inlist args, do: typespec_to_ast(arg)
247+
ast = { :::, [line: line], [{ name, [line: line], ast_args }, typespec_to_ast(result)] }
248+
249+
vars = args ++ [result]
250+
|> Enum.flat_map(&collect_vars/1)
251+
|> Enum.uniq
252+
|> Enum.map(&{ &1, { :var, [line: line], nil } })
253+
254+
unless vars == [] do
255+
ast = { :when, [line: line], [ast, vars] }
256+
end
257+
258+
ast
248259
end
249260

250261
def spec_to_ast(name, { :type, line, :fun, [] }) do
251262
{ :::, [line: line], [{ name, [line: line], [] }, quote(do: term)] }
252263
end
253264

254265
def spec_to_ast(name, { :type, line, :bounded_fun, [{ :type, _, :fun, [{ :type, _, :product, args }, result] }, constraints] }) do
255-
[h|t] =
256-
lc {:type, line, :constraint, [{:atom, _, :is_subtype}, [var, type]]} inlist constraints do
257-
{ :is_subtype, [line: line], [typespec_to_ast(var), typespec_to_ast(type)] }
266+
guards =
267+
lc {:type, _, :constraint, [{:atom, _, :is_subtype}, [{ :var, _, var }, type]]} inlist constraints do
268+
{ var, typespec_to_ast(type) }
258269
end
259270

260-
args = lc arg inlist args, do: typespec_to_ast(arg)
261-
guards = Enum.reduce t, h, fn(x, acc) -> { :and, line, [acc, x] } end
271+
ast_args = lc arg inlist args, do: typespec_to_ast(arg)
272+
273+
vars = args ++ [result]
274+
|> Enum.flat_map(&collect_vars/1)
275+
|> Enum.uniq
276+
277+
vars = vars -- Keyword.keys(guards)
278+
|> Enum.map(&{ &1, { :var, [line: line], nil } })
262279

263-
{ :when, [line: line], [{ :::, [line: line], [{ name, [line: line], args }, typespec_to_ast(result)] }, guards] }
280+
{ :when, [line: line], [{ :::, [line: line], [{ name, [line: line], ast_args }, typespec_to_ast(result)] }, guards ++ vars] }
264281
end
265282

266283
@doc """
@@ -418,28 +435,27 @@ defmodule Kernel.Typespec do
418435
end
419436

420437
@doc false
421-
def defspec(type, { :when, _, [{ :::, _, [{ name, meta, args }, return] }, constraints_guard] }, caller) do
438+
def defspec(type, { :when, meta2, [{ :::, _, [{ name, meta, args }, return] }, guard] }, caller) do
422439
if is_atom(args), do: args = []
423-
vars = guard_to_vars(constraints_guard)
424-
constraints = guard_to_constraints(constraints_guard, vars, caller)
440+
441+
unless Keyword.keyword?(guard) do
442+
guard = Macro.to_string(guard)
443+
compile_error caller, "invalid guard in function type specification `#{guard}`"
444+
end
445+
446+
vars = Keyword.keys(guard)
447+
constraints = guard_to_constraints(guard, vars, meta2, caller)
448+
425449
spec = { :type, line(meta), :fun, fn_args(meta, args, return, vars, caller) }
426450
if constraints != [] do
427451
spec = { :type, line(meta), :bounded_fun, [spec, constraints] }
428452
end
453+
429454
code = { { name, Kernel.length(args) }, spec }
430455
Module.compile_typespec(caller.module, type, code)
431456
code
432457
end
433458

434-
def defspec(type, { :when, _, [fun, { :::, _, [guards, return] }] } = spec, caller) do
435-
new_spec = { :when, [], [{ :::, [], [fun, return] }, guards] }
436-
IO.write "typespec format is deprecated `#{Macro.to_string(spec)}`\n" <>
437-
"new format is: `#{Macro.to_string(new_spec)}`\n" <>
438-
Exception.format_stacktrace
439-
440-
defspec(type, new_spec, caller)
441-
end
442-
443459
def defspec(type, { :::, _, [{ name, meta, args }, return] }, caller) do
444460
if is_atom(args), do: args = []
445461
spec = { :type, line(meta), :fun, fn_args(meta, args, return, [], caller) }
@@ -453,40 +469,48 @@ defmodule Kernel.Typespec do
453469
compile_error caller, "invalid function type specification `#{spec}`"
454470
end
455471

456-
defp guard_to_vars({ :is_subtype, _, [{ name, _, _ }, _] }) do
457-
[name]
472+
defp guard_to_constraints(guard, vars, meta, caller) do
473+
line = line(meta)
474+
475+
Enum.reduce(guard, [], fn
476+
{ _name, { :var, _, context } }, acc when is_atom(context) ->
477+
acc
478+
{ name, type }, acc ->
479+
constraint = [{ :atom, line, :is_subtype }, [{:var, line, name}, typespec(type, vars, caller)]]
480+
type = { :type, line, :constraint, constraint }
481+
[type|acc]
482+
end) |> Enum.reverse
458483
end
459484

460-
defp guard_to_vars({ :is_var, _, [{ name, _, _ }] }) do
461-
[name]
485+
## To AST conversion
486+
487+
defp collect_vars({ :ann_type, _line, args }) do
488+
Enum.flat_map(args, &collect_vars/1)
462489
end
463490

464-
defp guard_to_vars({ :and, _, [left, right] }) do
465-
guard_to_vars(left) ++ guard_to_vars(right)
491+
defp collect_vars({ :type, _line, _kind, args }) do
492+
Enum.flat_map(args, &collect_vars/1)
466493
end
467494

468-
defp guard_to_constraints({ :is_subtype, meta, [{ name, _, context }, type] }, vars, caller)
469-
when is_atom(name) and is_atom(context) do
470-
line = line(meta)
471-
contraints = [{ :atom, line, :is_subtype }, [{:var, line, name}, typespec(type, vars, caller)]]
472-
[{ :type, line, :constraint, contraints }]
495+
defp collect_vars({ :remote_type, _line, args }) do
496+
Enum.flat_map(args, &collect_vars/1)
473497
end
474498

475-
defp guard_to_constraints({ :is_var, _, [{ name, _, context }] }, _, _)
476-
when is_atom(name) and is_atom(context) do
477-
[]
499+
defp collect_vars({ :typed_record_field, _line, type }) do
500+
collect_vars(type)
478501
end
479502

480-
defp guard_to_constraints({ :and, _, [left, right] }, vars, caller) do
481-
guard_to_constraints(left, vars, caller) ++ guard_to_constraints(right, vars, caller)
503+
defp collect_vars({:paren_type, _line, [type]}) do
504+
collect_vars(type)
482505
end
483506

484-
defp guard_to_constraints(other, _vars, caller) do
485-
guard = Macro.to_string(other)
486-
compile_error caller, "invalid guard in function type specification `#{guard}`"
507+
defp collect_vars({:var, _line, var}) do
508+
[erl_to_ex_var(var)]
487509
end
488510

489-
## To AST conversion
511+
defp collect_vars(_) do
512+
[]
513+
end
490514

491515
defp typespec_to_ast({ :type, line, :tuple, :any }) do
492516
typespec_to_ast({:type, line, :tuple, []})
@@ -549,14 +573,7 @@ defmodule Kernel.Typespec do
549573
end
550574

551575
defp typespec_to_ast({ :var, line, var }) do
552-
var =
553-
case atom_to_binary(var) do
554-
<<"_", c :: [binary, size(1)], rest :: binary>> ->
555-
binary_to_atom("_#{String.downcase(c)}#{rest}")
556-
<<c :: [binary, size(1)], rest :: binary>> ->
557-
binary_to_atom("#{String.downcase(c)}#{rest}")
558-
end
559-
{ var, line, nil }
576+
{ erl_to_ex_var(var), line, nil }
560577
end
561578

562579
# Special shortcut(s)
@@ -598,6 +615,15 @@ defmodule Kernel.Typespec do
598615

599616
defp typespec_to_ast(other), do: other
600617

618+
defp erl_to_ex_var(var) do
619+
case atom_to_binary(var) do
620+
<<"_", c :: [binary, size(1)], rest :: binary>> ->
621+
binary_to_atom("_#{String.downcase(c)}#{rest}")
622+
<<c :: [binary, size(1)], rest :: binary>> ->
623+
binary_to_atom("#{String.downcase(c)}#{rest}")
624+
end
625+
end
626+
601627
## From AST conversion
602628

603629
defp line(meta) do

lib/elixir/lib/list.ex

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ defmodule List do
4848
iex> List.duplicate([1, 2], 2)
4949
[[1,2],[1,2]]
5050
"""
51-
@spec duplicate(elem, non_neg_integer) :: [elem] when is_var(elem)
51+
@spec duplicate(elem, non_neg_integer) :: [elem] when elem: var
5252
def duplicate(elem, n) do
5353
:lists.duplicate(n, elem)
5454
end
@@ -62,7 +62,7 @@ defmodule List do
6262
[1,2,3]
6363
6464
"""
65-
@spec flatten(deep_list) :: list when is_subtype(deep_list, [any | deep_list])
65+
@spec flatten(deep_list) :: list when deep_list: [any | deep_list]
6666
def flatten(list) do
6767
:lists.flatten(list)
6868
end
@@ -78,7 +78,7 @@ defmodule List do
7878
[1,2,3,4,5]
7979
8080
"""
81-
@spec flatten(deep_list, [elem]) :: [elem] when is_subtype(deep_list, [elem | deep_list]) and is_var(elem)
81+
@spec flatten(deep_list, [elem]) :: [elem] when elem: var, deep_list: [elem | deep_list]
8282
def flatten(list, tail) do
8383
:lists.flatten(list, tail)
8484
end
@@ -96,7 +96,7 @@ defmodule List do
9696
2
9797
9898
"""
99-
@spec foldl([elem], acc, (elem, acc -> acc)) :: acc when is_var(elem) and is_var(acc)
99+
@spec foldl([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var
100100
def foldl(list, acc, function) when is_list(list) and is_function(function) do
101101
:lists.foldl(function, acc, list)
102102
end
@@ -111,7 +111,7 @@ defmodule List do
111111
-2
112112
113113
"""
114-
@spec foldr([elem], acc, (elem, acc -> acc)) :: acc when is_var(elem) and is_var(acc)
114+
@spec foldr([elem], acc, (elem, acc -> acc)) :: acc when elem: var, acc: var
115115
def foldr(list, acc, function) when is_list(list) and is_function(function) do
116116
:lists.foldr(function, acc, list)
117117
end
@@ -129,7 +129,7 @@ defmodule List do
129129
3
130130
131131
"""
132-
@spec last([elem]) :: nil | elem when is_var(elem)
132+
@spec last([elem]) :: nil | elem when elem: var
133133

134134
def last([]), do: nil
135135

@@ -399,7 +399,7 @@ defmodule List do
399399
[1, 2, 3]
400400
401401
"""
402-
@spec update_at([elem], integer, (elem -> any)) :: list when is_var(elem)
402+
@spec update_at([elem], integer, (elem -> any)) :: list when elem: var
403403
def update_at(list, index, fun) do
404404
if index < 0 do
405405
do_update_at(list, length(list) + index, fun)

lib/elixir/lib/macro.ex

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ defmodule Macro do
4949
end
5050

5151
@doc """
52-
Breaks a pipeline expression into a list.
53-
52+
Breaks a pipeline expression into a list.
53+
5454
Raises if the pipeline is ill-formed.
5555
"""
5656
@spec unpipe(Macro.t) :: [Macro.t]
@@ -152,8 +152,8 @@ defmodule Macro do
152152
end
153153

154154
@doc %S"""
155-
Unescape the given chars.
156-
155+
Unescape the given chars.
156+
157157
This is the unescaping behavior
158158
used by default in Elixir single- and double-quoted strings.
159159
Check `unescape_string/2` for information on how to customize
@@ -298,7 +298,14 @@ defmodule Macro do
298298

299299
# Tuple containers
300300
def to_string({ :{}, _, args } = ast, fun) do
301-
fun.(ast, "{" <> Enum.map_join(args, ", ", &to_string(&1, fun)) <> "}")
301+
if match?([_], args) do
302+
tuple = "{" <> Enum.map_join(args, ", ", &to_string(&1, fun)) <> "}"
303+
else
304+
args = args_to_string(args, fun)
305+
tuple = "{" <> args <> "}"
306+
end
307+
308+
fun.(ast, tuple)
302309
end
303310

304311
# Fn keyword
@@ -321,6 +328,17 @@ defmodule Macro do
321328
fun.(ast, "(" <> arrow_to_string(ast, fun, true) <> ")")
322329
end
323330

331+
# left when right
332+
def to_string({ :when, _, [left, right] } = ast, fun) do
333+
if right != [] and Keyword.keyword?(right) do
334+
right = kw_list_to_string(right, fun)
335+
else
336+
right = fun.(ast, op_to_string(right, fun, :when, :right))
337+
end
338+
339+
fun.(ast, op_to_string(left, fun, :when, :left) <> " when " <> right)
340+
end
341+
324342
# Binary ops
325343
def to_string({ op, _, [left, right] } = ast, fun) when op in @binary_ops do
326344
fun.(ast, op_to_string(left, fun, op, :left) <> " #{op} " <> op_to_string(right, fun, op, :right))
@@ -362,11 +380,15 @@ defmodule Macro do
362380

363381
# Lists
364382
def to_string(list, fun) when is_list(list) do
365-
if Keyword.keyword?(list) do
366-
fun.(list, "[" <> kw_list_to_string(list, fun) <> "]")
367-
else
368-
fun.(list, "[" <> Enum.map_join(list, ", ", &to_string(&1, fun)) <> "]")
369-
end
383+
fun.(list, cond do
384+
Keyword.keyword?(list) ->
385+
"[" <> kw_list_to_string(list, fun) <> "]"
386+
not match?([_], list) ->
387+
args = args_to_string(list, fun)
388+
"[" <> args <> "]"
389+
true ->
390+
"[" <> Enum.map_join(list, ", ", &to_string(&1, fun)) <> "]"
391+
end)
370392
end
371393

372394
# All other structures
@@ -389,18 +411,20 @@ defmodule Macro do
389411
defp call_to_string(other, fun), do: to_string(other, fun)
390412

391413
defp call_to_string_with_args(target, args, fun) do
392-
{ list, last } = :elixir_utils.split_last(args)
393414
target = call_to_string(target, fun)
415+
args = args_to_string(args, fun)
416+
target <> "(" <> args <> ")"
417+
end
394418

395-
case last != [] and Keyword.keyword?(last) do
396-
true ->
397-
args = Enum.map_join(list, ", ", &to_string(&1, fun))
398-
if list != [], do: args = args <> ", "
399-
args = args <> kw_list_to_string(last, fun)
400-
target <> "(" <> args <> ")"
401-
false ->
402-
args = Enum.map_join(args, ", ", &to_string(&1, fun))
403-
target <> "(" <> args <> ")"
419+
defp args_to_string(args, fun) do
420+
{ list, last } = :elixir_utils.split_last(args)
421+
422+
if last != [] and Keyword.keyword?(last) do
423+
args = Enum.map_join(list, ", ", &to_string(&1, fun))
424+
if list != [], do: args = args <> ", "
425+
args <> kw_list_to_string(last, fun)
426+
else
427+
Enum.map_join(args, ", ", &to_string(&1, fun))
404428
end
405429
end
406430

@@ -483,8 +507,8 @@ defmodule Macro do
483507
end
484508

485509
@doc """
486-
Receives an AST node and expands it once.
487-
510+
Receives an AST node and expands it once.
511+
488512
The following contents are expanded:
489513
490514
* Macros (local or remote);
@@ -665,8 +689,8 @@ defmodule Macro do
665689

666690
@doc """
667691
Receives an AST node and expands it until it no longer represents
668-
a macro.
669-
692+
a macro.
693+
670694
Check `expand_once/2` for more information on how
671695
expansion works.
672696
"""
@@ -715,7 +739,7 @@ defmodule Macro do
715739
@doc """
716740
Recursively traverses the quoted expression checking if all sub-terms are
717741
safe.
718-
742+
719743
Terms are considered safe if they represent data structures and don't actually
720744
evaluate code. Returns `:ok` unless a given term is unsafe,
721745
which is returned as `{ :unsafe, term }`.

0 commit comments

Comments
 (0)