Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Runtime Type Safety for Erlang/OTP Behaviours

Runtime Type Safety for Erlang/OTP Behaviours

Joe Harrison

August 18, 2019
Tweet

Other Decks in Technology

Transcript

  1. RUNTIME TYPE SAFETY FOR ERLANG/OTP RUNTIME TYPE SAFETY FOR ERLANG/OTP

    BEHAVIOURS BEHAVIOURS (Presented at Erlang'19 in Berlin, Germany) JOE HARRISON JOE HARRISON 18TH AUGUST 2019 18TH AUGUST 2019 1
  2. INTRODUCTION INTRODUCTION Ongoing work to automatically detect communication discrepancies Partial

    formalisation of Core Erlang Sub-Typing for Erlang to check sends/receives between registered processes Protecting OTP processes by a combination of static/runtime analysis 2 . 1
  3. INTRODUCTION INTRODUCTION Ongoing work to automatically detect communication discrepancies Partial

    formalisation of Core Erlang Sub-Typing for Erlang to check sends/receives between registered processes Protecting OTP processes by a combination of static/runtime analysis A sound underapproximation of the requests accepted by OTP behaviour callbacks Doesn't find all bugs Only finds real bugs Only stops real crashes No false positives 2 . 1
  4. EXAMPLE: A BUGGY SERVER EXAMPLE: A BUGGY SERVER -module(my_server). -behaviour(gen_server).

    -export([start_link/0,get/0,put/1]). -export([init/1,handle_call/3,handle_cast/2]). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init(_) -> {ok, some_initial_state}. get() -> gen_server:call(?MODULE, get). put(State) -> gen_server:call(?MODULE, {put, State}). handle_call(get, _From, State) -> {reply, State, State}. handle_cast({put, State}, _) -> {noreply, State}. 3 . 1
  5. EXAMPLE: A BUGGY SERVER EXAMPLE: A BUGGY SERVER -module(my_server). -behaviour(gen_server).

    -export([start_link/0,get/0,put/1]). -export([init/1,handle_call/3,handle_cast/2]). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init(_) -> {ok, some_initial_state}. get() -> gen_server:call(?MODULE, get). put(State) -> gen_server:call(?MODULE, {put, State}). handle_call(get, _From, State) -> {reply, State, State}. handle_cast({put, State}, _) -> {noreply, State}. $ erl 1> c(my_server). {ok,my_server} 2> my_server:start_link(). {ok,<0.85.0>} 3> my_server:get(). some_initial_state 3 . 1
  6. EXAMPLE: A BUGGY SERVER EXAMPLE: A BUGGY SERVER -module(my_server). -behaviour(gen_server).

    -export([start_link/0,get/0,put/1]). -export([init/1,handle_call/3,handle_cast/2]). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init(_) -> {ok, some_initial_state}. get() -> gen_server:call(?MODULE, get). put(State) -> gen_server:call(?MODULE, {put, State}). handle_call(get, _From, State) -> {reply, State, State}. handle_cast({put, State}, _) -> {noreply, State}. $ erl 1> c(my_server). {ok,my_server} 2> my_server:start_link(). {ok,<0.85.0>} 3> my_server:get(). some_initial_state 4> my_server:put(some_precious_state). =ERROR REPORT==== ** Generic server my_server terminating ** Last message in was {put,some_precious_state} ** When Server state == some_initial_state ** Reason for termination == * {function_clause,[{my_server,handle_call, [{put,some_precious_state},... 3 . 1
  7. Q: What was the programmer's intention? A: Not sure. Not

    enough information. EXAMPLE: A BUGGY SERVER EXAMPLE: A BUGGY SERVER -module(my_server). -behaviour(gen_server). -export([start_link/0,get/0,put/1]). -export([init/1,handle_call/3,handle_cast/2]). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init(_) -> {ok, some_initial_state}. get() -> gen_server:call(?MODULE, get). put(State) -> gen_server:call(?MODULE, {put, State}). handle_call(get, _From, State) -> {reply, State, State}. handle_cast({put, State}, _) -> {noreply, State}. $ erl 1> c(my_server). {ok,my_server} 2> my_server:start_link(). {ok,<0.85.0>} 3> my_server:get(). some_initial_state 4> my_server:put(some_precious_state). =ERROR REPORT==== ** Generic server my_server terminating ** Last message in was {put,some_precious_state} ** When Server state == some_initial_state ** Reason for termination == * {function_clause,[{my_server,handle_call, [{put,some_precious_state},... 3 . 1
  8. APPROACH APPROACH 1. Static Analysis: Inferring the types of requests

    2. Runtime Verification: Determining whether requests satisfy type constraints 4 . 1
  9. APPROACH APPROACH 1. Static Analysis: Inferring the types of requests

    2. Runtime Verification: Determining whether requests satisfy type constraints 3. Hybrid analysis: Combining static analysis with runtime verification 4 . 1
  10. APPROACH APPROACH 1. Static Analysis: Inferring the types of requests

    2. Runtime Verification: Determining whether requests satisfy type constraints 3. Hybrid analysis: Combining static analysis with runtime verification 4. Implementation: Modifying gen_server to perform synchronous checking 4 . 1
  11. STATIC ANALYSIS STATIC ANALYSIS Need to determine which terms which

    are incompatible with a callback Callback function heads contain type information in patterns and guards 5 . 1
  12. STATIC ANALYSIS STATIC ANALYSIS Need to determine which terms which

    are incompatible with a callback Callback function heads contain type information in patterns and guards Infer types of patterns and guards to approximate types of callback args 5 . 1
  13. STATIC ANALYSIS EXAMPLE PROBLEM STATIC ANALYSIS EXAMPLE PROBLEM Given this

    code: What is the type of the sole argument of f? f(X) when is_atom(X) and not is_boolean(X) -> some:fun(); f({req, X}) when is_integer(X) -> some:fun(). 6 . 1
  14. INFERRING THE TYPES OF COMPATIBLE REQUESTS INFERRING THE TYPES OF

    COMPATIBLE REQUESTS Function heads contain type information about their arguments. 7 . 1
  15. INFERRING THE TYPES OF COMPATIBLE REQUESTS INFERRING THE TYPES OF

    COMPATIBLE REQUESTS Function heads contain type information about their arguments. f(X) when is_atom(X) and not is_boolean(X) -> some:fun(); f({req, X}) when is_integer(X) -> some:fun(). 7 . 1
  16. 1st arg must be: An atom and not a boolean;

    or A 2-tuple of 'req' and an integer INFERRING THE TYPES OF COMPATIBLE REQUESTS INFERRING THE TYPES OF COMPATIBLE REQUESTS Function heads contain type information about their arguments. f(X) when is_atom(X) and not is_boolean(X) -> some:fun(); f({req, X}) when is_integer(X) -> some:fun(). 7 . 1
  17. 1st arg must be: An atom and not a boolean;

    or A 2-tuple of 'req' and an integer INFERRING THE TYPES OF COMPATIBLE REQUESTS INFERRING THE TYPES OF COMPATIBLE REQUESTS Function heads contain type information about their arguments. (atom() ⊓ ¬boolean()) ⊔ ′ req ′ , integer() f(X) when is_atom(X) and not is_boolean(X) -> some:fun(); f({req, X}) when is_integer(X) -> some:fun(). { } 7 . 1
  18. 1st arg must be: An atom and not a boolean;

    or A 2-tuple of 'req' and an integer INFERRING THE TYPES OF COMPATIBLE REQUESTS INFERRING THE TYPES OF COMPATIBLE REQUESTS Function heads contain type information about their arguments. (atom() ⊓ ¬boolean()) ⊔ ′ req ′ , integer() How do we infer these types? f(X) when is_atom(X) and not is_boolean(X) -> some:fun(); f({req, X}) when is_integer(X) -> some:fun(). { } 7 . 1
  19. 1st arg must be: An atom and not a boolean;

    or A 2-tuple of 'req' and an integer INFERRING THE TYPES OF COMPATIBLE REQUESTS INFERRING THE TYPES OF COMPATIBLE REQUESTS Function heads contain type information about their arguments. (atom() ⊓ ¬boolean()) ⊔ ′ req ′ , integer() How do we infer these types? Infer types of guards and patterns, then combine the two. f(X) when is_atom(X) and not is_boolean(X) -> some:fun(); f({req, X}) when is_integer(X) -> some:fun(). { } 7 . 1
  20. X: integer() X: atom() ⊓ ¬boolean() INFERRING THE TYPES OF

    GUARDS INFERRING THE TYPES OF GUARDS is_integer(X) is_atom(X) and not is_boolean(X) 8 . 1
  21. X: integer() X: atom() ⊓ ¬boolean() X: [atom() | term()]

    INFERRING THE TYPES OF GUARDS INFERRING THE TYPES OF GUARDS is_integer(X) is_atom(X) and not is_boolean(X) is_atom(hd(X)) 8 . 1
  22. X: integer() X: atom() ⊓ ¬boolean() X: [atom() | term()]

    X: ′ foo ′ term() INFERRING THE TYPES OF GUARDS INFERRING THE TYPES OF GUARDS is_integer(X) is_atom(X) and not is_boolean(X) is_atom(hd(X)) hd(X) =:= 'foo' [ | ] 8 . 1
  23. empty_list() maybe_improper_nonempty_list() (not nonempty_list()!) ′ req ′ , term(), term()

    INFERRING THE TYPES OF PATTERNS INFERRING THE TYPES OF PATTERNS [] [H|T] {'req', X, Y} { } 9 . 1
  24. X: integer() INFERRING THE TYPES OF FUNCTION ARGUMENTS INFERRING THE

    TYPES OF FUNCTION ARGUMENTS is_integer(X) 10 . 1
  25. X: integer() ′ req ′ , term() INFERRING THE TYPES

    OF FUNCTION ARGUMENTS INFERRING THE TYPES OF FUNCTION ARGUMENTS is_integer(X) {req, _} { } 10 . 1
  26. X: integer() ′ req ′ , term() 1st arg: ′

    req ′ , integer() INFERRING THE TYPES OF FUNCTION ARGUMENTS INFERRING THE TYPES OF FUNCTION ARGUMENTS is_integer(X) {req, _} { } f({req, X}) when is_integer(X) -> { } 10 . 1
  27. X: integer() ′ req ′ , term() 1st arg: ′

    req ′ , integer() 1st arg: ′ req ′ , integer() INFERRING THE TYPES OF FUNCTION ARGUMENTS INFERRING THE TYPES OF FUNCTION ARGUMENTS is_integer(X) {req, _} { } f({req, X}) when is_integer(X) -> { } handle_cast({req, X}, _, State) when is_integer(X) -> { } 10 . 1
  28. ASIDE: INFERRING THE TYPES OF LISTS ASIDE: INFERRING THE TYPES

    OF LISTS Let's ask Erlang what it thinks a list is… 11 . 1
  29. ASIDE: INFERRING THE TYPES OF LISTS ASIDE: INFERRING THE TYPES

    OF LISTS Let's ask Erlang what it thinks a list is… $ erl 1> is_list([]). true 2> is_list([1,2,3]). true 3> is_list([ill|formed]). true 11 . 1
  30. is_list returns true for ill-formed lists infer maybe_improper_list() ASIDE: INFERRING

    THE TYPES OF LISTS ASIDE: INFERRING THE TYPES OF LISTS Let's ask Erlang what it thinks a list is… $ erl 1> is_list([]). true 2> is_list([1,2,3]). true 3> is_list([ill|formed]). true 11 . 1
  31. is_list returns true for ill-formed lists infer maybe_improper_list() [H|T] matches

    ill-formed lists infer maybe_improper_nonempty_list() ASIDE: INFERRING THE TYPES OF LISTS ASIDE: INFERRING THE TYPES OF LISTS Let's ask Erlang what it thinks a list is… $ erl 1> is_list([]). true 2> is_list([1,2,3]). true 3> is_list([ill|formed]). true 11 . 1
  32. is_list returns true for ill-formed lists infer maybe_improper_list() [H|T] matches

    ill-formed lists infer maybe_improper_nonempty_list() Recursion needed to infer proper list types: e.g. length(X) >= 0 ASIDE: INFERRING THE TYPES OF LISTS ASIDE: INFERRING THE TYPES OF LISTS Let's ask Erlang what it thinks a list is… $ erl 1> is_list([]). true 2> is_list([1,2,3]). true 3> is_list([ill|formed]). true 11 . 1
  33. is_list returns true for ill-formed lists infer maybe_improper_list() [H|T] matches

    ill-formed lists infer maybe_improper_nonempty_list() Recursion needed to infer proper list types: e.g. length(X) >= 0 Conclusion: patterns and type test BIFs alone cannot test for proper lists of indeterminate length ASIDE: INFERRING THE TYPES OF LISTS ASIDE: INFERRING THE TYPES OF LISTS Let's ask Erlang what it thinks a list is… $ erl 1> is_list([]). true 2> is_list([1,2,3]). true 3> is_list([ill|formed]). true 11 . 1
  34. STATIC ANALYSIS SUMMARY STATIC ANALYSIS SUMMARY Infer types of patterns

    and guards Combine to infer types of individual function arguments 12 . 1
  35. STATIC ANALYSIS SUMMARY STATIC ANALYSIS SUMMARY Infer types of patterns

    and guards Combine to infer types of individual function arguments Over-approximates types No equality or size constraints (e.g. {X, X}, length(X) > 2) Improper list types typically inferred (no recursion) No data flow analysis or consideration of side effects All type constraints are upper bounds Any terms not in the inferred types will not match 12 . 1
  36. STATIC ANALYSIS SUMMARY STATIC ANALYSIS SUMMARY Infer types of patterns

    and guards Combine to infer types of individual function arguments Over-approximates types No equality or size constraints (e.g. {X, X}, length(X) > 2) Improper list types typically inferred (no recursion) No data flow analysis or consideration of side effects All type constraints are upper bounds Any terms not in the inferred types will not match Bridge to runtime verification: cache inferred type information for use at runtime 12 . 1
  37. RUNTIME VERIFICATION RUNTIME VERIFICATION Compile-time type inference gives us types

    of callback arguments At runtime, we will have these inferred types and an incoming message 13 . 1
  38. RUNTIME VERIFICATION RUNTIME VERIFICATION Compile-time type inference gives us types

    of callback arguments At runtime, we will have these inferred types and an incoming message Question: is the message "compatible" with any of the types? 13 . 1
  39. RUNTIME VERIFICATION RUNTIME VERIFICATION Compile-time type inference gives us types

    of callback arguments At runtime, we will have these inferred types and an incoming message Question: is the message "compatible" with any of the types? If the message is not "compatible", an error will occur 13 . 1
  40. RUNTIME VERIFICATION EXAMPLE PROBLEM RUNTIME VERIFICATION EXAMPLE PROBLEM Given this

    type for the first argument of my_module:handle_call: (atom() ⊓ ¬boolean()) ⊔ integer() Will the following request definitely cause a crash? 1> {ok, Pid} = my_module:start_link(). 2> gen_server:call(Pid, '1.0'). 14 . 1
  41. [[term()]] = U (the universe of all Erlang terms) WHAT

    DO THE TYPES WHAT DO THE TYPES MEAN MEAN? ? 15 . 2
  42. [[term()]] = U (the universe of all Erlang terms) [[atom()]]

    = ′ a ′ , ′ foo ′ , ′ 0 ′ , … (all atoms) WHAT DO THE TYPES WHAT DO THE TYPES MEAN MEAN? ? { } 15 . 2
  43. [[term()]] = U (the universe of all Erlang terms) [[atom()]]

    = ′ a ′ , ′ foo ′ , ′ 0 ′ , … (all atoms) [[atom() ⊓ ¬boolean()]] = [[atom()]] ∖ [[boolean()]] (relative complement, set of atoms minus set of booleans) WHAT DO THE TYPES WHAT DO THE TYPES MEAN MEAN? ? { } 15 . 2
  44. [[term()]] = U (the universe of all Erlang terms) [[atom()]]

    = ′ a ′ , ′ foo ′ , ′ 0 ′ , … (all atoms) [[atom() ⊓ ¬boolean()]] = [[atom()]] ∖ [[boolean()]] (relative complement, set of atoms minus set of booleans) A type denotes a (possibly infinite) set of Erlang terms. This means the types must be handled symbolically. WHAT DO THE TYPES WHAT DO THE TYPES MEAN MEAN? ? { } 15 . 2
  45. OBTAINING THE TYPE OF A MESSAGE AT RUNTIME OBTAINING THE

    TYPE OF A MESSAGE AT RUNTIME 16 . 1
  46. ′ req ′ , 2 ′ req ′ , [1

    | [2 | []]] OBTAINING THE TYPE OF A MESSAGE AT RUNTIME OBTAINING THE TYPE OF A MESSAGE AT RUNTIME Erlang terms (and messages) have singleton types at runtime: {req, 2} { } {req, [1,2]} { } 16 . 1
  47. ′ req ′ , 2 ′ req ′ , [1

    | [2 | []]] OBTAINING THE TYPE OF A MESSAGE AT RUNTIME OBTAINING THE TYPE OF A MESSAGE AT RUNTIME Erlang terms (and messages) have singleton types at runtime: [[ ′ req ′ , 2 ]] = ′ req ′ , 2 "The set containing the term { ′ req ′ , 2}". Singletons do not have to be handled symbolically. {req, 2} { } {req, [1,2]} { } { } {{ }} 16 . 1
  48. Message type ′ req ′ , ′ foo ′ MATCHING

    REQUEST TYPES TO CALLBACK ARGUMENT TYPES MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES { } 17 . 1
  49. Message type ′ req ′ , ′ foo ′ Inferred

    callback message type {term(), atom() ⊓ ¬boolean()} MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES { } 17 . 1
  50. Message type ′ req ′ , ′ foo ′ Inferred

    callback message type {term(), atom() ⊓ ¬boolean()} MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES Message type ≠ inferred callback message type, but… { } 17 . 1
  51. Message type ′ req ′ , ′ foo ′ Inferred

    callback message type {term(), atom() ⊓ ¬boolean()} MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES Message type ≠ inferred callback message type, but… Message type is compatible with inferred type { } 17 . 1
  52. Message type ′ req ′ , ′ foo ′ Inferred

    callback message type {term(), atom() ⊓ ¬boolean()} MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES MATCHING REQUEST TYPES TO CALLBACK ARGUMENT TYPES Message type ≠ inferred callback message type, but… Message type is compatible with inferred type This compatibility is via sub-typing { } 17 . 1
  53. SUB-TYPING IN ERLANG SUB-TYPING IN ERLANG Type S is compatible

    with type T if S is a sub-type of T: S ≤ T 18 . 1
  54. SUB-TYPING IN ERLANG SUB-TYPING IN ERLANG Type S is compatible

    with type T if S is a sub-type of T: S ≤ T S is a sub-type of T if (and only if): [[S]] ⊆ [[T]] i.e.: S ≤ T ⟺ [[S]] ⊆ [[T]] 18 . 1
  55. SUB-TYPING IN ERLANG SUB-TYPING IN ERLANG Type S is compatible

    with type T if S is a sub-type of T: S ≤ T S is a sub-type of T if (and only if): [[S]] ⊆ [[T]] i.e.: S ≤ T ⟺ [[S]] ⊆ [[T]] S ≤ T implies that terms of type S can be used where terms of type T are expected ′ req ′ , ′ foo ′ ≤ {term(), atom() ⊓ ¬boolean()} { } 18 . 1
  56. SUB-TYPING AS CALLBACK COMPATIBILITY SUB-TYPING AS CALLBACK COMPATIBILITY handle_call({double, X},

    _, State) when is_number(X) -> {reply, X+X, State}. $ erl 1> V = 2. 2> is_integer(V). true 3> gen_server:call(Pid, {double, 2}). 4 20 . 1
  57. SUB-TYPING AS CALLBACK COMPATIBILITY SUB-TYPING AS CALLBACK COMPATIBILITY A message

    of type S is compatible handle_call's 1st arg of type T if S ≤ T. handle_call({double, X}, _, State) when is_number(X) -> {reply, X+X, State}. $ erl 1> V = 2. 2> is_integer(V). true 3> gen_server:call(Pid, {double, 2}). 4 20 . 1
  58. SUB-TYPING AS CALLBACK COMPATIBILITY SUB-TYPING AS CALLBACK COMPATIBILITY A message

    of type S is compatible handle_call's 1st arg of type T if S ≤ T. Furthermore, as a message with type S is always a singleton at runtime, S is incompatible with T if S ≰ T. handle_call({double, X}, _, State) when is_number(X) -> {reply, X+X, State}. $ erl 1> V = 2. 2> is_integer(V). true 3> gen_server:call(Pid, {double, 2}). 4 20 . 1
  59. CALLBACK COMPATIBILITY VIA SUB-TYPING CALLBACK COMPATIBILITY VIA SUB-TYPING S ≰

    T does not mean the request will fail in the general case 21 . 1
  60. CALLBACK COMPATIBILITY VIA SUB-TYPING CALLBACK COMPATIBILITY VIA SUB-TYPING S ≤

    T means it should be safe to pass terms of type S to callback with arg type T 21 . 2
  61. CALLBACK COMPATIBILITY VIA SUB-TYPING CALLBACK COMPATIBILITY VIA SUB-TYPING A message

    is a singleton at runtime, so we can check compatibility via set membership ( ∈ ). 21 . 3
  62. INFERRING TYPES AND SAVING METADATA INFERRING TYPES AND SAVING METADATA

    Embedding performed via a parse transform 1. Find known OTP behaviours (check behaviour attr) 2. Infer types (static analysis) 3. Embed type_info function (accessible at runtime) 23 . 1
  63. INTERCEPTING MESSAGES BEFORE DISPATCH INTERCEPTING MESSAGES BEFORE DISPATCH Modifying gen_server

    module. Example of modifications to handle_call dispatcher: try_handle_call(Mod, Msg, From, State) -> case msg_compatible(Msg, Mod, handle_call) of ok -> %% original dispatch code Error -> Error end. msg_compatible(Msg, Mod, Func) -> MsgType = type_of_term(Msg), CallbackArgType = Mod:type_info(Func), case is_subtype(MsgType, CallbackArgType) of true -> ok; {false, _Ty} -> {type_error, Msg, MsgType} end. 25 . 1
  64. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ 26 . 1
  65. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Sub-typing is equivalent to checking an intersection on sets: S ≤ T ⟺ [[S]] ∩ [[¬T]] = ∅ 26 . 1
  66. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Sub-typing is equivalent to checking an intersection on sets: S ≤ T ⟺ [[S]] ∩ [[¬T]] = ∅ but types might be infinite, so checking non-trivial types is not straightforward: ((atom() ⊔ ¬integer()) ⊓ (boolean() ⊔ number()) ≤ (atom() ⊔ ¬boolean()) ⊓ (term() ⊓ ¬float()) 26 . 1
  67. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Sub-typing is equivalent to checking an intersection on sets: S ≤ T ⟺ [[S]] ∩ [[¬T]] = ∅ but types might be infinite, so checking non-trivial types is not straightforward: ((atom() ⊔ ¬integer()) ⊓ (boolean() ⊔ number()) ≤ (atom() ⊔ ¬boolean()) ⊓ (term() ⊓ ¬float()) Idea: rewrite types into a canonical form and compare syntactically 26 . 1
  68. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ 26 . 2
  69. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Intersection of "positive" types is easy atom() ⊓ boolean() ⟶ boolean() atom() ⊓ number() ⟶ none() 26 . 2
  70. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Intersection of "positive" types is easy atom() ⊓ boolean() ⟶ boolean() atom() ⊓ number() ⟶ none() We can rewrite all types into DNF and apply intersections 26 . 2
  71. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Intersection of "positive" types is easy atom() ⊓ boolean() ⟶ boolean() atom() ⊓ number() ⟶ none() We can rewrite all types into DNF and apply intersections Drawback: relatively slow (exponential in the worst case) 26 . 2
  72. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Intersection of "positive" types is easy atom() ⊓ boolean() ⟶ boolean() atom() ⊓ number() ⟶ none() We can rewrite all types into DNF and apply intersections Drawback: relatively slow (exponential in the worst case) Advantage: sound and complete w.r.t. set-theoretic interpretation of types: canonicalise(S ⊓ ¬T) = none() ⟺ [[S]] ⊆ [[T]] 26 . 2
  73. TYPE CHECKING IN THE PRESENCE OF TYPE CHECKING IN THE

    PRESENCE OF ¬ ¬, , ⊓ ⊓ , AND , AND ⊔ ⊔ Intersection of "positive" types is easy atom() ⊓ boolean() ⟶ boolean() atom() ⊓ number() ⟶ none() We can rewrite all types into DNF and apply intersections Drawback: relatively slow (exponential in the worst case) Advantage: sound and complete w.r.t. set-theoretic interpretation of types: canonicalise(S ⊓ ¬T) = none() ⟺ [[S]] ⊆ [[T]] "Better" approach: use graph representation of types 26 . 2
  74. atom() ⊓ ¬boolean() TYPE CHECKING VIA BINARY DECISION DIAGRAMS TYPE

    CHECKING VIA BINARY DECISION DIAGRAMS 27 . 1
  75. atom() ⊓ ¬boolean() also atom() ⊓ ¬boolean() TYPE CHECKING VIA

    BINARY DECISION DIAGRAMS TYPE CHECKING VIA BINARY DECISION DIAGRAMS 27 . 1
  76. TYPE CHECKING VIA BINARY DECISION DIAGRAMS TYPE CHECKING VIA BINARY

    DECISION DIAGRAMS boolean() ≤ atom() is equivalent to [[boolean()]] ∩ [[¬atom()]] = ∅ 27 . 2
  77. boolean() ¬atom() TYPE CHECKING VIA BINARY DECISION DIAGRAMS TYPE CHECKING

    VIA BINARY DECISION DIAGRAMS boolean() ≤ atom() is equivalent to [[boolean()]] ∩ [[¬atom()]] = ∅ 27 . 2
  78. boolean() ¬atom() boolean() ⊓ ¬atom() Unsatisfiable, so sub-typing relation holds.

    TYPE CHECKING VIA BINARY DECISION DIAGRAMS TYPE CHECKING VIA BINARY DECISION DIAGRAMS boolean() ≤ atom() is equivalent to [[boolean()]] ∩ [[¬atom()]] = ∅ 27 . 2
  79. SUB-TYPING SUMMARY SUB-TYPING SUMMARY Request allowed to pass if S

    ≤ T S is type of request T is type of corresponding callback argument 28 . 1
  80. SUB-TYPING SUMMARY SUB-TYPING SUMMARY Request allowed to pass if S

    ≤ T S is type of request T is type of corresponding callback argument Sub-typing algorithm with ⊓ , ⊔ , and ¬ is difficult Use binary decision diagrams instead Insight: S will be a singleton at runtime 28 . 1
  81. SUB-TYPING SUMMARY SUB-TYPING SUMMARY Request allowed to pass if S

    ≤ T S is type of request T is type of corresponding callback argument Sub-typing algorithm with ⊓ , ⊔ , and ¬ is difficult Use binary decision diagrams instead Insight: S will be a singleton at runtime Request should be rejected if S ≰ T 28 . 1
  82. RETURNING ERRORS TO CLIENTS RETURNING ERRORS TO CLIENTS gen_server replies

    with a $gen_client_error tuple message when type errors occur in try_handle_call do_call(Process, Label, Request, Timeout) when is_atom(Process) =:= false -> Mref = erlang:monitor(process, Process), erlang:send(Process, {Label, {self(), Mref}, Request}, [noconnect]), receive {Mref, '$gen_client_error', Error} -> erlang:demonitor(Mref, [flush]), {error, Error}; {Mref, Reply} -> erlang:demonitor(Mref, [flush]), {ok, Reply}; %% monitor statuses and timeouts follow 29 . 1
  83. Interacting with the server: END RESULT: FINDING END RESULT: FINDING

    REAL REAL BUGS BUGS -module(my_server). -behaviour(ts_gen_server). -export([start_link/0,get/0,put/1]). -export([init/1,handle_call/3,handle_cast/2]). start_link() -> ts_gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init(_) -> {ok, some_initial_state}. get() -> ts_gen_server:call(?MODULE, get). put(State) -> ts_gen_server:call(?MODULE, {put, State}). handle_call(get, _From, State) -> {reply, State, State}. handle_cast({put, State}, _) -> {noreply, State}. 1> c(my_server). {ok,my_server} 2> my_server:start_link(). {ok,<0.85.0>} 3> my_server:get(). some_initial_state 30 . 1
  84. Interacting with the server: Trying to put the server state:

    END RESULT: FINDING END RESULT: FINDING REAL REAL BUGS BUGS -module(my_server). -behaviour(ts_gen_server). -export([start_link/0,get/0,put/1]). -export([init/1,handle_call/3,handle_cast/2]). start_link() -> ts_gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init(_) -> {ok, some_initial_state}. get() -> ts_gen_server:call(?MODULE, get). put(State) -> ts_gen_server:call(?MODULE, {put, State}). handle_call(get, _From, State) -> {reply, State, State}. handle_cast({put, State}, _) -> {noreply, State}. 1> c(my_server). {ok,my_server} 2> my_server:start_link(). {ok,<0.85.0>} 3> my_server:get(). some_initial_state 4> my_server:put(some_precious_state). ** exception error {bad_call_type,{'put','some_precious_state'}} 30 . 1
  85. Interacting with the server: Trying to put the server state:

    But the server survives END RESULT: FINDING END RESULT: FINDING REAL REAL BUGS BUGS -module(my_server). -behaviour(ts_gen_server). -export([start_link/0,get/0,put/1]). -export([init/1,handle_call/3,handle_cast/2]). start_link() -> ts_gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init(_) -> {ok, some_initial_state}. get() -> ts_gen_server:call(?MODULE, get). put(State) -> ts_gen_server:call(?MODULE, {put, State}). handle_call(get, _From, State) -> {reply, State, State}. handle_cast({put, State}, _) -> {noreply, State}. 1> c(my_server). {ok,my_server} 2> my_server:start_link(). {ok,<0.85.0>} 3> my_server:get(). some_initial_state 4> my_server:put(some_precious_state). ** exception error {bad_call_type,{'put','some_precious_state'}} 5> my_server:get(). some_initial_state 30 . 1
  86. CONTRIBUTIONS & CONCLUSIONS CONTRIBUTIONS & CONCLUSIONS Lightweight static analysis to

    infer callback argument types Similar goals to Gradualiser, TypEr Automatic embedding of type information via parse transform Accessible programmatically at runtime Runtime verification via sound and complete sub-typing algorithm Only prevents real crashes as type constraints are under-approximated Proof-of-concept modification of gen_server 31 . 1
  87. FUTURE WORK FUTURE WORK Integrate with more powerful static analysis

    tool Gradualiser, Dialyzer Perform more static analysis to correllate client and server code Look for registered names, data flow analysis of PIDs Detect definitely incompatible requests at compile time Mark definitely safe requests at compile time: bypass runtime checks 32 . 1
  88. Joe Harrison [email protected] slides: PDF: Title background by Håkon Sataøen

    https://sigwinch.uk/erlang19.pdf https://unsplash.com/photos/Oog0wehKxYs 33 . 1