Skip to content

Avoid broadcast in getfields/getproperties - #109

Open
ChrisRackauckas-Claude wants to merge 4 commits into
JuliaObjects:masterfrom
ChrisRackauckas-Claude:inv/generated-getfields-no-broadcast
Open

ChrisRackauckas-Claude wants to merge 4 commits into
JuliaObjects:masterfrom
ChrisRackauckas-Claude:inv/generated-getfields-no-broadcast

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown

Summary

  • Make default getfields a @generated expansion of getfield calls, and route the common getproperties path (when propertynames === fieldnames) through it.
  • For custom / non-field propertynames, build values with an explicit ntuple of getproperty instead of getproperty.(…).
  • Documents why the broadcast form is problematic (semantic docs updated to an ntuple sketch).

Why not broadcast?

The previous implementation was essentially:

getproperties(obj) = tuple_or_ntuple(names, getproperty.((obj,), names))
getfields(obj)     = NamedTuple{fnames}(getfield.((obj,), fnames))

Broadcasting over a property-name tuple (NTuple{N,Symbol} or similar) compiles broadcast / convert MethodInstances on fully open object types. Those instances live in dependents' precompile caches (e.g. SciMLBase remake via ConstructionBase.getproperties on ODEFunction). Loading packages that add BroadcastStyle methods — notably Symbolics — invalidates them; PrecompileTools.@recompile_invalidations then tries to rebuild them and can hit Julia's

AssertionError: irinterp is unable to handle heavy recursion correctly

during ModelingToolkit precompilation (discourse #139291, SciMLBase #1595).

Semantics are unchanged; only the default implementation avoids those broadcast edges. SciMLBase #1595 is a scoped workaround for AbstractSciMLFunction; this PR is the general upstream fix.

Test plan

  • Pkg.test() on Julia 1.12.7 (full suite, including inference / no-alloc / custom propertynames / StaticArrays / IntervalSets)
  • CI

Made with Cursor

The previous getproperty./getfield. form over property-name tuples compiles broadcast/convert MethodInstances that Symbolics-style BroadcastStyle methods invalidate; rebuilding them under @recompile_invalidations can hit Julia's irinterp recursion limit. Use a generated getfields path (and ntuple for custom propertynames) instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ChrisRackauckas

Copy link
Copy Markdown

This is because the broadcast relies on type inference recursion, which is both slow and hits limits. So the library shouldn't rely on that. Discourse posts show breakdowns on types with large numbers of type parameters.

@jw3126

jw3126 commented Sep 9, 2026

Copy link
Copy Markdown
Member

cool, can you bump the version?

@ChrisRackauckas

Copy link
Copy Markdown

Done

@jw3126

jw3126 commented Sep 9, 2026

Copy link
Copy Markdown
Member

Lets not touch the .md files. I feel this is more noise than useful.

@aplavin

aplavin commented Sep 9, 2026 via email

Copy link
Copy Markdown
Member

Comment thread src/ConstructionBase.jl Outdated
Co-authored-by: Christopher Rackauckas <accounts@chrisrackauckas.com>
Comment thread src/getproperties.md Outdated
Comment thread src/getproperties.md Outdated
Comment thread src/getfields.md Outdated
Co-authored-by: Christopher Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas

Copy link
Copy Markdown

Lets not touch the .md files. I feel this is more noise than useful.

Okay, I removed the comments about why it's not broadcasting from the .md, but I kept the changes pointing people down a path that is easier on the compiler. Since I think a lot of people will just copy what the docs do, we might as well tell them to do ntuple instead of broadcast, though sure they don't necessarily need the explanation as to why.

Super verbose comments, docs, typical claude :) Any chance this can be leaner?Also, whats a specific example that is fixed, before/after behavior?On Sep 9, 2026 06:32, Jan Weidner @.***> wrote:jw3126 left a comment (JuliaObjects/ConstructionBase.jl#109)

I made the comments just a quick line and a link back to the original issue that describes why this formulation is done. That should be small but sufficient enough for someone to understand the code.

@aplavin

aplavin commented Sep 9, 2026

Copy link
Copy Markdown
Member

What does it actually fix, do you have some direct example showing difference before-after?
Also, does it perform correctly for a structure with fieldnames being the same as propertynames, but fields still != properties?

@ChrisRackauckas

Copy link
Copy Markdown

What does it actually fix, do you have some direct example showing difference before-after?

Yes, see the OP. It has links to https://discourse.julialang.org/t/modelingtoolkit-internal-error-in-runtime/139291

┌ ModelingToolkit
│  Internal error: during type inference of
│  convert(Type{var"#s175"} where var"#s175"<:Tuple{Tuple{SciMLBase.ODEFunction{iip, specialize, F, TMM, Ta, Tt, TJ, JVP, VJP, JP, SP, TW, TWt, WP, TPJ, VJP_P, O, TCV, SYS, ID, NLP} where NLP<:Union{Nothing, SciMLBase.ODENLStepData{NLProb, SetU0, SetGammaC, SetOuterTmp, SetInnerTmp, NLProbMap} where NLProbMap where SetInnerTmp where SetOuterTmp where SetGammaC where SetU0 where NLProb} where ID<:Union{Nothing, SciMLBase.OverrideInitData{IProb, UIProb, IProbMap, IProbPmap, M, OOP} where OOP<:Union{Base.Val{true}, Base.Val{false}} where M where IProbPmap where IProbMap where UIProb where IProb} where SYS where TCV where O where VJP_P where TPJ where WP where TWt where TW where SP where JP where VJP where JVP where TJ where Tt where Ta where TMM where F where specialize where iip}, NTuple{19, Symbol}}, Tuple{Tuple{SciMLBase.ODEFunction{iip, specialize, F, TMM, Ta, Tt, TJ, JVP, VJP, JP, SP, TW, TWt, WP, TPJ, VJP_P, O, TCV, SYS, ID, NLP} where NLP<:Union{Nothing, SciMLBase.ODENLStepData{NLProb, SetU0, SetGammaC, SetOuterTmp, SetInnerTmp, NLProbMap} where NLProbMap where SetInnerTmp where SetOuterTmp where SetGammaC where SetU0 where NLProb} where ID<:Union{Nothing, SciMLBase.OverrideInitData{IProb, UIProb, IProbMap, IProbPmap, M, OOP} where OOP<:Union{Base.Val{true}, Base.Val{false}} where M where IProbPmap where IProbMap where UIProb where IProb} where SYS where TCV where O where VJP_P where TPJ where WP where TWt where TW where SP where JP where VJP where JVP where TJ where Tt where Ta where TMM where F where specialize where iip}, NTuple{19, Symbol}})
│  Encountered unexpected error in runtime:
│  AssertionError(msg="irinterp is unable to handle heavy recursion correctly")
│  collect_limitations! at .\../usr/share/julia/Compiler/src/ssair\irinterp.jl:4 [inlined]
│  from_interprocedural! at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:422 [inlined]
│  infercalls at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:277
│  jfptr_infercalls_78525.1 at C:\Users\ufechner\.julia\juliaup\julia-1.12.7+0.x64.w64.mingw32\lib\julia\sys.dll (unknown line)
│  doworkloop at .\../usr/share/julia/Compiler/src\inferencestate.jl:1205
│  reprocess_instruction! at .\../usr/share/julia/Compiler/src/ssair\irinterp.jl:159
│  #224 at .\../usr/share/julia/Compiler/src/ssair\irinterp.jl:364
│  scan! at .\../usr/share/julia/Compiler/src/ssair\irinterp.jl:286
│  #ir_abstract_constant_propagation#220 at .\../usr/share/julia/Compiler/src/ssair\irinterp.jl:332
│  ir_abstract_constant_propagation at .\../usr/share/julia/Compiler/src/ssair\irinterp.jl:318
│  semi_concrete_eval_call at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:1266
│  abstract_call_method_with_const_args at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:893
│  abstract_call_method_with_const_args at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:868
│  handle1 at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:178
│  infercalls at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:252
│  abstract_call_gf_by_type at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:338
│  abstract_call_unknown at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:2891
│  abstract_call at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:2901
│  abstract_call at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:2897 [inlined]
│  abstract_call at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:3057
│  abstract_eval_call at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:3075 [inlined]
│  abstract_eval_statement_expr at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:3404
│  abstract_eval_basic_statement at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:3850 [inlined]
│  abstract_eval_basic_statement at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:3807 [inlined]
│  typeinf_local at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:4357
│  jfptr_typeinf_local_80842.1 at C:\Users\ufechner\.julia\juliaup\julia-1.12.7+0.x64.w64.mingw32\lib\julia\sys.dll (unknown line)
│  typeinf at .\../usr/share/julia/Compiler/src\abstractinterpretation.jl:4515
│  typeinf_ext at .\../usr/share/julia/Compiler/src\typeinfer.jl:1256
│  typeinf_ext_toplevel at .\../usr/share/julia/Compiler/src\typeinfer.jl:1439 [inlined]
│  typeinf_ext_toplevel at .\../usr/share/julia/Compiler/src\typeinfer.jl:1448
│  jfptr_typeinf_ext_toplevel_80922.1 at C:\Users\ufechner\.julia\juliaup\julia-1.12.7+0.x64.w64.mingw32\lib\julia\sys.dll (unknown line)
│  _jl_invoke at C:/workdir/src\gf.c:3995 [inlined]
│  ijl_apply_generic at C:/workdir/src\gf.c:4214 [inlined]
│  jl_apply at C:/workdir/src\julia.h:2394 [inlined]
│  jl_type_infer at C:/workdir/src\gf.c:466
│  _generate_from_hint at C:/workdir/src\gf.c:3834
│  jl_compile_now at C:/workdir/src\gf.c:3848 [inlined]
│  ijl_compile_method_instance at C:/workdir/src\gf.c:3861
│  precompile at .\loading.jl:4315 [inlined]
│  precompile at .\loading.jl:4315 [inlined]
│  precompile_mi at C:\Users\ufechner\.julia\packages\PrecompileTools\QUxvR\src\PrecompileTools.jl:11 [inlined]
│  foreach at .\abstractarray.jl:3191
│  recompile_invalidations at C:\Users\ufechner\.julia\packages\PrecompileTools\QUxvR\src\invalidations.jl:27
│  unknown function (ip: 0000022f8c8a84a3) at (unknown file)
│  jl_apply at C:/workdir/src\julia.h:2394 [inlined]
│  do_call at C:/workdir/src\interpreter.c:123
│  eval_value at C:/workdir/src\interpreter.c:243
│  eval_stmt_value at C:/workdir/src\interpreter.c:194 [inlined]
│  eval_body at C:/workdir/src\interpreter.c:707
│  jl_interpret_toplevel_thunk at C:/workdir/src\interpreter.c:898
│  top-level scope at C:\Users\ufechner\.julia\packages\ModelingToolkit\zh12i\src\ModelingToolkit.jl:27
│  jl_toplevel_eval_flex at C:/workdir/src\toplevel.c:1035
│  jl_eval_module_expr at C:/workdir/src\toplevel.c:197 [inlined]
│  jl_toplevel_eval_flex at C:/workdir/src\toplevel.c:814
│  jl_toplevel_eval_flex at C:/workdir/src\toplevel.c:975
│  jl_toplevel_eval_flex at C:/workdir/src\toplevel.c:975
│  ijl_toplevel_eval at C:/workdir/src\toplevel.c:1047 [inlined]
│  ijl_toplevel_eval_in at C:/workdir/src\toplevel.c:1092
│  eval at .\boot.jl:489
│  include_string at .\loading.jl:2952
│  _include at .\loading.jl:3012
│  include at .\Base.jl:306
│  include_package_for_output at .\loading.jl:3106
│  jfptr_include_package_for_output_71677.1 at C:\Users\ufechner\.julia\juliaup\julia-1.12.7+0.x64.w64.mingw32\lib\julia\sys.dll (unknown line)
│  jl_apply at C:/workdir/src\julia.h:2394 [inlined]
│  do_call at C:/workdir/src\interpreter.c:123
│  eval_value at C:/workdir/src\interpreter.c:243
│  eval_stmt_value at C:/workdir/src\interpreter.c:194 [inlined]
│  eval_body at C:/workdir/src\interpreter.c:707
│  jl_interpret_toplevel_thunk at C:/workdir/src\interpreter.c:898
│  top-level scope at stdin:5
│  jl_toplevel_eval_flex at C:/workdir/src\toplevel.c:1035
│  jl_toplevel_eval_flex at C:/workdir/src\toplevel.c:975
│  ijl_toplevel_eval at C:/workdir/src\toplevel.c:1047 [inlined]
│  ijl_toplevel_eval_in at C:/workdir/src\toplevel.c:1092
│  eval at .\boot.jl:489
│  include_string at .\loading.jl:2952
│  include_string at .\loading.jl:2962 [inlined]
│  exec_options at .\client.jl:315
│  _start at .\client.jl:550
│  jfptr__start_59066.1 at C:\Users\ufechner\.julia\juliaup\julia-1.12.7+0.x64.w64.mingw32\lib\julia\sys.dll (unknown line)
│  jl_apply at C:/workdir/src\julia.h:2394 [inlined]
│  true_main at C:/workdir/src\jlapi.c:985
│  jl_repl_entrypoint at C:/workdir/src\jlapi.c:1153
│  mainCRTStartup at C:/workdir/cli\loader_exe.c:58
│  BaseThreadInitThunk at C:\Windows\System32\KERNEL32.DLL (unknown line)
│  RtlUserThreadStart at C:\Windows\SYSTEM32\ntdll.dll (unknown line)
└

With this, that is gone. Well, I also did the same thing downstream so that it's already fixed for users of SciML, but any similar type would also give a similar error right now on main.

Also, does it perform correctly for a structure with fieldnames being the same as propertynames, but fields still != properties?

Not sure what you mean. It's literally the exact same Julia code, just not using broadcast.

@jw3126 jw3126 closed this Sep 11, 2026
@jw3126 jw3126 reopened this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants