# Infiltrator.jl: a no-nonsense debugging

# Introduction

During JuliaCon 2023 I have had several discussions about setup of my working environment when I develop Julia code. One of the common questions was tools that I use to debug my code.

With Julia an important aspect of the debugging tool one uses is to ensure that it does not have a significant impact on the performance of code execution. Keeping this condition in mind, I find Infiltrator.jl quite convenient. In this post I want to show an example how this package can be used.

The post was written using Julia 1.9.2 and Infiltrator.jl 1.6.4. Note that you should use the examples that I present in the terminal using the standard Julia REPL.

# An example project

Let us write a simple function that merges two sorted vectors into a new sorted vector:

```
function mergesorted(x::T, y::T) where {T<:Vector}
@assert issorted(x) && issorted(y)
z = T(undef, length(x) + length(y))
ix, iy, iz = 1, 1, 1
for iz in eachindex(z)
if x[ix] < y[iy]
z[iz] = x[ix]
ix += 1
else
z[iz] = y[iy]
iy += 1
end
end
return z
end
```

Now test it on some example data:

```
julia> mergesorted([1, 3], [2, 4])
ERROR: BoundsError: attempt to access 2-element Vector{Int64} at index [3]
```

We see that we get a problem when we try to get an element from a vector with a too large index. Let us try to infiltrate this issue but providing an appropriate condition when we want to investigate the state of our function:

```
using Infiltrator
function mergesorted(x::T, y::T) where {T<:Vector}
@assert issorted(x) && issorted(y)
z = T(undef, length(x) + length(y))
ix, iy, iz = 1, 1, 1
for iz in eachindex(z)
@infiltrate ix > length(x) || iy > length(y)
if x[ix] < y[iy]
z[iz] = x[ix]
ix += 1
else
z[iz] = y[iy]
iy += 1
end
end
return z
end
```

The magic of `@infiltrate ix > length(x) || iy > length(y)`

is that the execution of the `mergesorted`

function will
be interrupted at the moment when the passed condition is met.
Our condition is that either `ix`

or `iy`

index gets too large.

Let us run our test with an updated definition of the function:

```
julia> mergesorted([1, 3], [2, 4])
Infiltrating mergesorted(x::Vector{Int64}, y::Vector{Int64})
at REPL[11]:6
infil> @locals
- x::Vector{Int64} = [1, 3]
- T::DataType = Vector{Int64}
- iz::Int64 = 4
- y::Vector{Int64} = [2, 4]
- z::Vector{Int64} = [1, 2, 3, 2041652468496]
- iy::Int64 = 2
- ix::Int64 = 3
infil> @continue
ERROR: BoundsError: attempt to access 2-element Vector{Int64} at index [3]
```

We see that we have a problem that we get beyond the end of the
`x`

vector while the last element of the `y`

vector is still not
processed. It looks that we need to check if we are beyond
the end of the `x`

vector, and if it is the case jump right
to the `else`

part of the code:

```
function mergesorted(x::T, y::T) where {T<:Vector}
@assert issorted(x) && issorted(y)
z = T(undef, length(x) + length(y))
ix, iy, iz = 1, 1, 1
for iz in eachindex(z)
@infiltrate ix > length(x) || iy > length(y)
if ix <= length(x) && x[ix] < y[iy]
z[iz] = x[ix]
ix += 1
else
z[iz] = y[iy]
iy += 1
end
end
return z
end
```

Let us run the code:

```
julia> mergesorted([1, 3], [2, 4])
Infiltrating mergesorted(x::Vector{Int64}, y::Vector{Int64})
at REPL[13]:6
infil> @locals
- x::Vector{Int64} = [1, 3]
- T::DataType = Vector{Int64}
- iz::Int64 = 4
- y::Vector{Int64} = [2, 4]
- z::Vector{Int64} = [1, 2, 3, 8589934594]
- iy::Int64 = 2
- ix::Int64 = 3
infil> @continue
4-element Vector{Int64}:
1
2
3
4
```

This time things seem to work as expected. Let us thus turn-off infiltration and run some randomized tests:

```
function mergesorted(x::T, y::T) where {T<:Vector}
@assert issorted(x) && issorted(y)
z = T(undef, length(x) + length(y))
ix, iy, iz = 1, 1, 1
for iz in eachindex(z)
if ix <= length(x) && x[ix] < y[iy]
z[iz] = x[ix]
ix += 1
else
z[iz] = y[iy]
iy += 1
end
end
return z
end
```

And run the tests:

```
julia> using Random
julia> using Test
julia> Random.seed!(1234);
julia> for i in 1:10
x = sort!(rand(rand(1:10)))
y = sort!(rand(rand(1:10)))
@assert mergesorted(x, y) == sort!([x; y])
end
ERROR: BoundsError: attempt to access 3-element Vector{Float64} at index [4]
```

As a side note, the `rand(rand(1:10))`

is a convenient pattern for generating
random vectors of random length.

Going back to our main topic we see that we still get a problem. How to diagnose it? This time, as an example, let me show how to turn infiltration on when an error happens:

```
function mergesorted(x::T, y::T) where {T<:Vector}
@assert issorted(x) && issorted(y)
z = T(undef, length(x) + length(y))
ix, iy, iz = 1, 1, 1
for iz in eachindex(z)
try
if ix <= length(x) && x[ix] < y[iy]
z[iz] = x[ix]
ix += 1
else
z[iz] = y[iy]
iy += 1
end
catch e
@infiltrate
rethrow(e)
end
end
return z
end
```

Now run the same test code:

```
julia> Random.seed!(1234);
julia> for i in 1:10
x = sort!(rand(rand(1:10)))
y = sort!(rand(rand(1:10)))
@assert mergesorted(x, y) == sort!([x; y])
end
Infiltrating mergesorted(x::Vector{Float64}, y::Vector{Float64})
at REPL[31]:15
infil> @locals
- x::Vector{Float64} = [0.6932923170086805, 0.7600131804670265]
- T::DataType = Vector{Float64}
- iz::Int64 = 4
- y::Vector{Float64} = [0.11679226454435099, 0.20295936651757684, 0.43552097477865936]
- e::BoundsError = BoundsError([0.11679226454435099, 0.20295936651757684, 0.43552097477865936], (4,))
- z::Vector{Float64} = [0.11679226454435099, 0.20295936651757684, 0.43552097477865936, 5.0e-324, 5.0e-324]
- iy::Int64 = 4
- ix::Int64 = 1
infil> @continue
ERROR: BoundsError: attempt to access 3-element Vector{Float64} at index [4]
```

Ah - we can now see where the problem is. We have not covered the case when `iy`

is greater than
the length of `y`

. It seems we are close. Let us make a final attempt:

```
function mergesorted(x::T, y::T) where {T<:Vector}
@assert issorted(x) && issorted(y)
z = T(undef, length(x) + length(y))
ix, iy, iz = 1, 1, 1
for iz in eachindex(z)
if ix <= length(x) && (iy > length(y) || x[ix] < y[iy])
z[iz] = x[ix]
ix += 1
else
z[iz] = y[iy]
iy += 1
end
end
return z
end
```

We are ready for an even more comprehensive test:

```
julia> Random.seed!(1234);
julia> for i in 1:10_000
x = sort!(rand(rand(0:10)))
y = sort!(rand(rand(0:10)))
@assert mergesorted(x, y) == sort!([x; y])
end
```

This time we got no errors, so we can be relatively confident that our code works well.

# Conclusions

I find Infiltrator.jl useful because it is a lightweight solution. I did not discuss all of its features. However, even these minimal examples I have shown are quite nice in practice:

- You can use
`@infiltrate`

with a condition. - You can put
`@infiltrate`

in a`try-catch-end`

block to be able to infiltrate into the state of your computations at the moment an exception is thrown (a thing that I quite often need in practice).

Happy debugging!