Tab-Completion Internals
This page explains how MRITwixTools.jl achieves deep REPL tab-completion through the entire header tree. This is a developer-facing design document — for user-facing header usage, see Header Access.
The Problem
Julia's REPL tab-completion uses compile-time type inference (Core.Compiler.return_type) to determine what getproperty returns, then calls propertynames on that inferred type. With a plain Dict{String, Any}, the inferred return type is Any, and completion stops after the first level.
This means a naive implementation like:
struct SimpleHeader
data::Dict{String, Any} # values can be sub-dicts or leaves
end
Base.getproperty(h::SimpleHeader, name::Symbol) = getfield(h, :data)[String(name)]...would only offer tab-completion at the first level. hdr.MeasYaps.<TAB> would produce nothing.
The Solution: Split Storage in NestedDict
MRITwixTools.jl splits internal storage into two typed dictionaries:
struct NestedDict
_subtrees::Dict{String, NestedDict} # child NestedDicts
_leaves::Dict{String, Any} # leaf values (numbers, strings, etc.)
endA type-annotated helper ensures the compiler sees NestedDict as the return type when accessing subtrees:
_getprop_subtree(n::NestedDict, key::String)::NestedDict = getfield(n, :_subtrees)[key]
function Base.getproperty(n::NestedDict, name::Symbol)
key = String(name)
subtrees = getfield(n, :_subtrees)
if haskey(subtrees, key)
return _getprop_subtree(n, key) # compiler infers ::NestedDict
end
leaves = getfield(n, :_leaves)
if haskey(leaves, key)
return leaves[key]
end
error("NestedDict has no key: $key")
endThe key insight is the ::NestedDict return type annotation on _getprop_subtree. The compiler follows this path, infers NestedDict as the return type, and calls propertynames(::NestedDict) — which returns all keys from both _subtrees and _leaves. This enables unlimited chained tab-completion through the entire header tree.
TwixObj Return Type Narrowing
The same technique applies to TwixObj:
_twixobj_val(t::TwixObj, key::String)::Union{TwixHdr, RawData} = getfield(t, :_data)[key]Julia's REPL handles small Union types well — it offers propertynames from both TwixHdr and RawData, with the correct ones appearing at runtime based on evaluation.
TwixHdr Delegation
TwixHdr wraps a NestedDict and delegates getproperty to it:
function Base.getproperty(h::TwixHdr, name::Symbol)
name === :data && return getfield(h, :data)
nd = getfield(h, :data)
haskey(nd, key) && return getproperty(nd, name) # delegates to NestedDict
...
endThis preserves the ::NestedDict return type inference through the TwixHdr layer, so tab-completion flows through twixObj.hdr.MeasYaps.sKSpace.<TAB> seamlessly.
The getfield Consequence
Because getproperty is overridden on all major types, internal code must always use getfield(obj, :field) instead of obj.field when accessing actual struct fields. The helper functions (_st, _lv, _has, _get, _set!, _del!) encapsulate this for NestedDict.
See the Architecture page for more details on this rule.
Why Not PropertyDicts.jl?
PropertyDicts.jl is the closest existing Julia package, but it returns Any from getproperty, which breaks REPL inference. If Julia's REPL ever gains evaluation-based completion (instead of inference-based), PropertyDicts.jl + a search function would suffice and MRITwixTools.jl's split-storage approach could be simplified.
Numeric String Keys
ASCCONV array indices like asNucleusInfo[0] become string keys "0" in the NestedDict tree. These are included in propertynames output (via an all(isdigit, k) check) so tab-completion lists them alongside named keys.