# Constant propagation magic

**URL:** <https://discourse.julialang.org/t/constant-propagation-magic/135817>\
**Category:** General Usage\
**Tags:** question\
**Created:** [February 24, 2026, 8:25pm UTC](https://discourse.julialang.org/t/constant-propagation-magic/135817 "2026-02-24T20:25:19Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![michakraus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/michakraus/32/222105_2.png) [@michakraus](https://discourse.julialang.org/u/michakraus)\
**Post date:** [February 24, 2026, 8:25pm UTC](https://discourse.julialang.org/t/constant-propagation-magic/135817/1 "2026-02-24T20:25:19Z")

</div>

I carried out a little study to better understand type-stable overloading of `getproperty` and `getindex` with `NamedTuple` by using `Val`. Please see the (more or less) MWE below.

I think I understand almost everything in that script, except why the second to last example is type stable:

```julia-auto
function test_symbols(wrapper, keys)
    for k in keys
        @inferred wrapper[Val(k)]
    end
end

test_symbols(wr, (:t, :x))

```

Why has passing several keys in a `Tuple` and wrapping them into `Val` in a loop an advantage over passing just a single key as `Symbol` and wrapping it into `Val` as in the last example (where the wrapping happens in `getindex`)?  
I see why the last example is not type stable, but I do not see why the second to last example is type stable.

```julia-auto
using Test

# The type `Wrapper` has only one field that is a `NamedTuple`
struct Wrapper{wrapperType<:NamedTuple}
    data::wrapperType
end

function Wrapper(data::NamedTuple)
    Wrapper{typeof(data)}(data)
end

# `hasproperty` checks if `s` is either a key of the `NamedTuple` or a field in `Wrapper`
function Base.hasproperty(::Wrapper{WT}, s::Symbol) where {WT}
    hasfield(WT, s) || hasfield(Wrapper, s)
end

# `getproperty` checks if `s` is a key of the `NamedTuple` and if so returns the corresponding value,
# if not it returns the corresponding field of `Wrapper`
function Base.getproperty(w::Wrapper{WT}, s::Symbol) where {WT}
    if hasfield(WT, s)
        return getfield(getfield(w, :data), s)
    else
        return getfield(w, s)
    end
end

# Return wrapped NamedTuple
Base.parent(w::Wrapper) = w.data

# This getindex method recieves the key symbol wrapped in Val,
# which implies that the value is known at compile time
Base.getindex(w::Wrapper, ::Val{s}) where {s} = getindex(parent(w), s)

# This getindex method wraps the key symbol into a Val, and passes it on to the above method
Base.getindex(w::Wrapper, s::Symbol) = getindex(w, Val(s))

### Test for type stability ###

# Create a `Wrapper` instance based on some arbitrary data (with different types)

data = (
    t=0.0,
    x=rand(3),
)

wr = Wrapper(data)

# Accessing fields of NamedTuple via forwarding in getproperty is type-stable
# as the value of the key symbol is known at compile time:

test_t(wrapper) = wrapper.t
test_x(wrapper) = wrapper.x

@inferred test_t(wr)
@inferred test_x(wr)

# Accessing fields of NamedTuple via getindex of Val{<:Symbol} is type stable

test_symbol(wrapper, s) = wrapper[s]

@inferred test_symbol(wr, Val(:t))
@inferred test_symbol(wr, Val(:x))

@inferred wr[Val(:t)]
@inferred wr[Val(:x)]

# Accessing fields of NamedTuple via getindex of Val{<:Symbol} with
# Val wrapping inside a loop is also type stable

function test_symbols(wrapper, keys)
    for k in keys
        @inferred wrapper[Val(k)]
    end
end

test_symbols(wr, (:t, :x))

# Accessing fields of NamedTuple via getindex of Symbol that wraps the key
# symbol into a Val is not type stable

@inferred test_symbol(wr, :t)
@inferred test_symbol(wr, :x)

```

This was tested on

```julia-auto
Julia Version 1.13.0-beta2
Commit f36fbbfd951 (2026-02-04 11:07 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: macOS (arm64-apple-darwin24.0.0)
  CPU: 16 × Apple M4 Max
  WORD_SIZE: 64
  LLVM: libLLVM-20.1.8 (ORCJIT, apple-m4)
  GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 12 virtual cores)

```

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [February 24, 2026, 8:30pm UTC](https://discourse.julialang.org/t/constant-propagation-magic/135817/2 "2026-02-24T20:30:32Z")

</div>

Quick answer: I suspect it’s not, not really.

The `@inferred` macro _dynamically_ looks at the types of the arguments of the _outermost_ function, and just ensures that the function called _with those types_ is type stable. In this case, that’s the `getindex` call (from the `[]` syntax). If you look at `@code_warntype test_symbols(wr, (:t, :x))`, I’ll wager there’s a whole slew of red.

Now, it _might_ _also_ still be type stable through some constant propagation magic in some compiled contexts, but I don’t think that’s what’s happening here.

---

<div class="post-metadata">

**Author:** ![michakraus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/michakraus/32/222105_2.png) [@michakraus](https://discourse.julialang.org/u/michakraus)\
**Post date:** [February 25, 2026, 11:06am UTC](https://discourse.julialang.org/t/constant-propagation-magic/135817/3 "2026-02-25T11:06:36Z")

</div>

Thanks a lot for the swift reply and the explanation. That nailed it.

---

<div class="post-metadata">

**Author:** ![michakraus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/michakraus/32/222105_2.png) [@michakraus](https://discourse.julialang.org/u/michakraus)\
**Post date:** [February 25, 2026, 11:18am UTC](https://discourse.julialang.org/t/constant-propagation-magic/135817/4 "2026-02-25T11:18:05Z")

</div>

Actually, I realised that I made a mistake in my `@code_warntype` tests. With

```julia-auto
function test_symbols(wrapper, keys)
    (wrapper[Val(k)] for k in keys)
end

```

everything still seems stable:

```julia-auto
julia> @code_warntype test_symbols(wr, (:t, :x))
MethodInstance for test_symbols(::Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}}, ::Tuple{Symbol, Symbol})
  from test_symbols(wrapper, keys) @ Main REPL[44]:1
Arguments
  #self#::Core.Const(Main.test_symbols)
  wrapper::Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}}
  keys::Tuple{Symbol, Symbol}
Locals
  #7::var"#test_symbols##0#test_symbols##1"{Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}}}
Body::Base.Generator{Tuple{Symbol, Symbol}, var"#test_symbols##0#test_symbols##1"{Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}}}}
1 ─ %1 = Main.:(var"#test_symbols##0#test_symbols##1")::Core.Const(var"#test_symbols##0#test_symbols##1")
│ %2 = Core._typeof_captured_variable(wrapper)::Core.Const(Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}})
│ %3 = Core.apply_type(%1, %2)::Core.Const(var"#test_symbols##0#test_symbols##1"{Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}}})
│ (#7 = %new(%3, wrapper))
│ %5 = #7::var"#test_symbols##0#test_symbols##1"{Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}}}
│ %6 = Base.Generator(%5, keys)::Base.Generator{Tuple{Symbol, Symbol}, var"#test_symbols##0#test_symbols##1"{Wrapper{@NamedTuple{t::Float64, x::Vector{Float64}}}}}
└── return %6

```
