Symbolic Calls

Learn about symbolic calls and how they make debugging a property failure easier.

We'll cover the following

What are symbolic calls?

Some of the data structures or states that we need to generate for tests can be opaque and difficult to decipher. Think of debugging a binary protocol by looking at the individual bytes once the whole thing is encoded, or creating a process and sending it a bunch of messages to prime its state. The output of a failing property will be a lot of hard-to-read bits and bytes, or a term like <0.213.0>, which is not very helpful regardless of how much shrinking we may apply to them. The solution to this problem is a special category of generators built from symbolic calls.

A symbolic call is just a special notation for function calls so that they can be represented as data in generators. Rather than executing the operation straight away, the calls are built up as a data structure. Once the generator materializes, they are executed at once. The notation for them is {call, Module, Function, ArgumentList}.This is the format supported by PropEr. Another format, {$call, Module, Function, ArgumentList}, named automated symbolic call, is similar, but a bit easier in practice:

Function Call Symbolic Call Automated Symbolic Call (PropEr only)
sets:new() {call, sets, new, []} {'$call', sets, new, []}
queue:join(Q1, Q2) {call, queue, join, [Q1, Q2]} {'$call', queue, join, [Q1, Q2]}
lists:sort([1,2,3]) {call, lists, sort, [[1,2,3]]} {'$call', lists, sort, [[1,2,3]]}
local(Arg) (if exported) {call, ?MODULE, local, [Arg]} {'$call', ?MODULE, local, [Arg]}

Using either format of symbolic calls can make things simpler when looking at shrunken results. Let’s try an example using the dict data structure, which can be opaque if we don’t know how it’s implemented.

Example

Take a look at an example to learn the difference between symbolic calls and automated symbolic calls.

Get hands-on with 1200+ tech skills courses.