REPL crashes with custom pretty printing and Dictionaries

The REPL crashes when I run the following code twice, the first time giving the expected StackOverflowError:

using Dictionaries: dictionary

struct Bcode
    code::Int
end

Base.convert(::Type{String}, b::Bcode) = dictionary([Bcode(1) => "Z"])[b]
Base.show(io::IO, c::Bcode) = print(io, "$(convert(String, c))")

julia> Bcode(1)
Z

julia> Bcode(2)
Error showing value of type Bcode:
ERROR: StackOverflowError:
<snip>

julia> Bcode(2)
PS C:\Users\michele.zaffalon>

Additional information:

(test2) pkg> st
Status `C:\Users\michele.zaffalon\test2\Project.toml`
  [85a47980] Dictionaries v0.3.24

julia> versioninfo()
Julia Version 1.8.1
Commit afb6c60d69 (2022-09-06 15:09 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
  CPU: 6 × Intel(R) Core(TM) i5-8500 CPU @ 3.00GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-13.0.1 (ORCJIT, skylake)
  Threads: 1 on 6 virtual cores
Environment:
  JULIA_DEPOT_PATH = C:\Users\michele.zaffalon\.julia
  JULIA_PKG_USE_CLI_GIT = true

This would be expected to throw an error for any b not equal to Bcode(1). You are seeing a StackOverflowError in place of the true error, which should be something like

ERROR: IndexError("Dictionary does not contain index: Bcode(50)")
Stacktrace:
 [1] getindex(dict::Dictionary{Bcode, String}, i::Bcode)
   @ Dictionaries ~/.julia/packages/Dictionaries/8bcEH/src/AbstractDictionary.jl:46

I do expect a StackOverflowError every time I try to print anything except for Bcode(1). What happens is that the REPL crashes the second time I try.

Should I report this?

It’s not clear to me what you are trying to achieve with what appears to be an unusual convert method. I think you would need to explain this reason, in addition to the odd behaviour.

Perhaps worth reporting. I can’t reproduce the crash on Ubuntu, so this might be a Windows behavior.

1 Like

I have a mapping between Bcodes and Strings: for reasons of dispatch, I want to use Bcodes but for output to REPL I prefer the mnemonic names.

This is how I should have implemented it: the constructor prevents to create a Bcode for which there is no entry in the dictionary.

using Dictionaries: dictionary

const _bcode = dictionary([1 => "Z"])

struct Bcode
    code::Int
    Bcode(c) =
        c in keys(_bcode) ? new(c) : error("invalid code $c")
end

Base.convert(::Type{String}, b::Bcode) = _bcode[b.code]
Base.show(io::IO, c::Bcode) = print(io, "$(convert(String, c))")

The version I first posted was the first iteration of the code.

When Dictionary does not find Bcode(2) as a key in the dictionary, it wants to print an error message:

ERROR: Dictionaries.IndexError("Dictionary does not contain index: ***Bcode(2)***")

But the ***Bcode(2)*** in this hypothetical error message is the output of stringifying Bcode(2), which prints this error. So an infinite recursion loop is called which isn’t infinite, because the call stack is exhausted, and StackOverflowError triggered.
To avoid this, perhaps getkey should be used instead of indexing into dictionary. getkey has a default value. The following:

Base.convert(::Type{String}, b::Bcode) = getkey(Dict([Bcode(1) => "Z"]), b, "not found")

Doesn’t crash.

@Dan I totally agree with your analysis and with that of @jd-foster. Enforcing the check in the constructor is another way to prevent the error.

I only wanted to report that Julia quits the second time I generate the error.