Skip to content

Commit

Permalink
Merge pull request #112 from Qqwy/port_and_ref
Browse files Browse the repository at this point in the history
Support for the Port and Reference built-in types.
  • Loading branch information
Qqwy committed May 28, 2022
2 parents 331a735 + cea02fe commit 8c6f251
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 3 deletions.
6 changes: 3 additions & 3 deletions Comparing TypeCheck and Elixir Typespecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ In the tables below:
| atom() || |
| map() || any map |
| pid() || process identifier |
| port() | | port identifier |
| reference() | | |
| port() | | port identifier |
| reference() | | |
| tuple() || tuple of any size |
| float() || |
| integer() || |
Expand Down Expand Up @@ -83,7 +83,7 @@ This wrapper will only run once the the function actually is called.
| nonempty_charlist() || [char(), ...] |
| fun() || (... -> any) |
| function() || fun() |
| identifier() | | pid() \| port() \| reference() |
| identifier() | | pid() \| port() \| reference() |
| iodata() || iolist() \| binary() |
| iolist() || maybe_improper_list(byte() \| binary() \| iolist(), binary() \| []) |
| keyword() || [{atom(), any()}] |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ Details:
- It is not yet possible to combine them with fixed keys.
Because of this, the inspection of the builtin type `map(key, value)` has been changed to look the same as an optional map. _This is a minor backwards-incompatible change._
- Desugaring `%{}` has changed from 'any map' to 'the empty map' in line with Elixir's Typespecs. _This is a minor backwards-incompatible change._
- Support for `port()`, `reference()` and `identifier()`.

- 0.10.8 -
- Fixes:
Expand Down
52 changes: 52 additions & 0 deletions lib/type_check/builtin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,58 @@ defmodule TypeCheck.Builtin do
build_struct(TypeCheck.Builtin.PID)
end

@doc typekind: :builtin
@doc """
Matches any reference.
c.f. `TypeCheck.Builtin.Reference`
iex> TypeCheck.conforms?(Kernel.make_ref(), reference())
true
iex> some_ref = IEx.Helpers.ref(0, 749884137, 111673345, 43386)
...> TypeCheck.conforms!(some_ref, reference())
#Reference<0.749884137.111673345.43386>
"""
if_recompiling? do
@spec! reference() :: TypeCheck.Builtin.Reference.t()
end
def reference() do
build_struct(TypeCheck.Builtin.Reference)
end


@doc typekind: :builtin
@doc """
Matches any port.
c.f. `TypeCheck.Builtin.Port`
iex> TypeCheck.conforms?(Kernel.make_ref(), reference())
true
iex> some_port = Port.open({:spawn, "cat"}, [:binary])
...> TypeCheck.conforms?(some_port, port())
true
"""
if_recompiling? do
@spec! port() :: TypeCheck.Builtin.Port.t()
end
def port() do
build_struct(TypeCheck.Builtin.Port)
end

@doc typekind: :builtin
@doc """
Syntactic sugar for `pid() | port() | reference()`
iex> TypeCheck.conforms?(self(), identifier())
true
"""
def identifier() do
named_type(:identifier,
one_of([pid(), port(), reference()])
)
end

@doc typekind: :builtin
@doc """
A nonempty_list is any list with at least one element.
Expand Down
50 changes: 50 additions & 0 deletions lib/type_check/builtin/port.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule TypeCheck.Builtin.Port do
@moduledoc """
Type to check whether the given input is any port.
Check `Port` for more information on ports.
NOTE: The property testing generator will generate ports of the `cat` binary
which is a sensible default as it will send back any binaries sent to it exactly.
However, note that generated ports are not automatically closed.
"""
defstruct []

use TypeCheck
@type! t :: %__MODULE__{}
@type! problem_tuple :: {t(), :no_match, %{}, any()}

defimpl TypeCheck.Protocols.ToCheck do
def to_check(s, param) do
quote generated: true, location: :keep do
case unquote(param) do
x when is_port(x) ->
{:ok, [], x}
_ ->
{:error, {unquote(Macro.escape(s)), :no_match, %{}, unquote(param)}}
end
end
end
end

defimpl TypeCheck.Protocols.Inspect do
def inspect(_, opts) do
"port()"
|> Inspect.Algebra.color(:builtin_type, opts)
end
end

if Code.ensure_loaded?(StreamData) do
defimpl TypeCheck.Protocols.ToStreamData do
def to_gen(_s) do
# Ensure every iteration we create a _different_ port
{}
|> StreamData.constant()
|> StreamData.map(fn _ ->
Port.open({:spawn, "cat"}, [:binary])
end)
end
end
end
end
48 changes: 48 additions & 0 deletions lib/type_check/builtin/reference.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule TypeCheck.Builtin.Reference do
@moduledoc """
Type to check whether the given input is a reference.
Elixir/Erlang uses references for two use-cases:
1. As unique identifiers.
2. To refer to resources created and returned by NIFs (to be passed to other NIFs of the same NIF module).
The property testing generator will generate arbitrary references using `Kernel.make_ref()`.
To property-test the second kind of data, you should create your own kind of generator
that calls the appropriate NIF.
"""
defstruct []

use TypeCheck
@type! t :: %__MODULE__{}
@type! problem_tuple :: {t(), :no_match, %{}, any()}

defimpl TypeCheck.Protocols.ToCheck do
def to_check(s, param) do
quote generated: :true, location: :keep do
case unquote(param) do
x when is_reference(x) ->
{:ok, [], x}
_ ->
{:error, {unquote(Macro.escape(s)), :no_match, %{}, unquote(param)}}
end
end
end
end

defimpl TypeCheck.Protocols.Inspect do
def inspect(_, opts) do
"reference()"
|> Inspect.Algebra.color(:builtin_type, opts)
end
end

if Code.ensure_loaded?(StreamData) do
defimpl TypeCheck.Protocols.ToStreamData do
def to_gen(_s) do
# Ensure every iteration we create a _different_ reference
StreamData.constant({})
|> StreamData.map(fn _ -> make_ref() end)
end
end
end
end
2 changes: 2 additions & 0 deletions lib/type_check/protocols/inspect.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ structs = [
TypeCheck.Builtin.Number,
TypeCheck.Builtin.OneOf,
TypeCheck.Builtin.PID,
TypeCheck.Builtin.Port,
TypeCheck.Builtin.Reference,
TypeCheck.Builtin.Range,
TypeCheck.Builtin.SizedBitstring,
TypeCheck.Builtin.Tuple,
Expand Down
8 changes: 8 additions & 0 deletions lib/type_check/type_error/default_formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ defmodule TypeCheck.TypeError.DefaultFormatter do
"`#{inspect(val, inspect_value_opts())}` is not a pid."
end

def do_format({%TypeCheck.Builtin.Port{}, :no_match, _, val}) do
"`#{inspect(val, inspect_value_opts())}` is not a port."
end

def do_format({%TypeCheck.Builtin.Reference{}, :no_match, _, val}) do
"`#{inspect(val, inspect_value_opts())}` is not a reference."
end

def do_format({s = %TypeCheck.Builtin.Range{}, :not_an_integer, _, val}) do
compound_check(val, s, "`#{inspect(val, inspect_value_opts())}` is not an integer.")
end
Expand Down
2 changes: 2 additions & 0 deletions lib/type_check/type_error/formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ defmodule TypeCheck.TypeError.Formatter do
| TypeCheck.Builtin.OneOf.problem_tuple()
| TypeCheck.Builtin.PosInteger.problem_tuple()
| TypeCheck.Builtin.PID.problem_tuple()
| TypeCheck.Builtin.Port.problem_tuple()
| TypeCheck.Builtin.Reference.problem_tuple()
| TypeCheck.Builtin.Range.problem_tuple()
| TypeCheck.Builtin.Tuple.problem_tuple()

Expand Down
6 changes: 6 additions & 0 deletions test/type_check/builtin_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ defmodule TypeCheck.BuiltinTest do
quote do
<<_ :: 4 >>
end => TypeCheck.Builtin.SizedBitstring,
quote do
reference()
end => TypeCheck.Builtin.Reference,
quote do
port()
end => TypeCheck.Builtin.Port,
}

for {type, module} <- possibilities do
Expand Down

0 comments on commit 8c6f251

Please sign in to comment.