Enzyme cannot deduce type for integral function in reverse mode AD

Could someone explain why I’m getting the following error from this example?

using Enzyme

struct Parameters
    M::Int
    R::Real
end

function integrand2(v::Vector, nt::NamedTuple)
    x = v[1]
    y = v[2]
    θ = nt.θ
    dx = x - nt.params.R * cos(θ)
    dy = y - nt.params.R * sin(θ)
    return nt.params.M*hypot(dx, dy)
end

params = Parameters(1, 0.5)
v = [1.0, 2.0]
nt = NamedTuple{(:θ, :params)}((π / 2.0, params))
@show integrand2(v, nt)
@show autodiff(Reverse, integrand2, Active, Const(v), Active(nt))
julia> include("./enzyme-bug.jl");
integrand2(v, nt) = 1.8027756377319946
ERROR: LoadError: Enzyme could not find shadow for value
Current scope: 
preprocess_julia_integrand2_2825_inner.1{} addrspace(10)* ({} addrspace(10)*, { double, { i64, {} addrspace(10)* } })
cannot find shadow for { double, { i64, {} addrspace(10)* } } %1


Stacktrace:
  [1] integrand2
    @ ~/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:0 [inlined]
  [2] augmented_julia_integrand2_2825_inner_1wrap
    @ ~/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:0
  [3] macro expansion
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:5445 [inlined]
  [4] enzyme_call
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4983 [inlined]
  [5] AugmentedForwardThunk
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4919 [inlined]
  [6] autodiff
    @ ~/.julia/packages/Enzyme/g1jMR/src/Enzyme.jl:396 [inlined]
  [7] autodiff(::ReverseMode{…}, ::typeof(integrand2), ::Type{…}, ::Const{…}, ::Active{…})
    @ Enzyme ~/.julia/packages/Enzyme/g1jMR/src/Enzyme.jl:524
  [8] macro expansion
    @ show.jl:1181 [inlined]
  [9] top-level scope
    @ ~/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:22
 [10] include(fname::String)
    @ Base.MainInclude ./client.jl:494
 [11] top-level scope
    @ REPL[3]:1
in expression starting at /home/jwalker/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:22
Some type information was truncated. Use `show(err)` to see complete types.

This is an MWE for a problem that I’ve encountered with Integrals.jl. I need to pass some custom structs into the integrand via a (named) tuple of parameters, but I get this error when attempting to take the derivative.

Active is only meant for pass by value (aka float) data, for ease here, how about doing

Enzyme.gradient(Reverse, integrand2, Const(v), nt)

The problem here is the abstractly typed field R::Real, which makes the NamedTuple unsuited for an Active annotation. The following works:

using Enzyme

struct Parameters
    M::Int
    R::Float64
end

function integrand2(v::Vector, nt::NamedTuple)
    x = v[1]
    y = v[2]
    (; θ, params) = nt
    dx = x - params.R * cos(θ)
    dy = y - params.R * sin(θ)
    return params.M * hypot(dx, dy)
end

v = [1.0, 2.0]
nt = (; θ=π / 2.0, params=Parameters(1, 0.5))
@show integrand2(v, nt)
@show autodiff(Reverse, integrand2, Active, Const(v), Active(nt))
julia> include("enzyme-works.jl");
integrand2(v, nt) = 1.8027756377319946
autodiff(Reverse, integrand2, Active, Const(v), Active(nt)) = ((nothing, (θ = 0.2773500981126146, params = Parameters(0, -0.8320502943378437))),)
2 Likes

Great. It fixes that problem. Thanks!

Now, for the MWE that matches more closely my real problem, I have

using Enzyme
using Integrals

struct Parameters
    M::Int64
    R::Float64
end

function integrand(v::Vector{T}, nt::NamedTuple) where {T<:Real}
    x = v[1]
    y = v[2]
    θ = nt.θ
    return distance(x, y, θ, nt.params)
end

function distance(x::T, y::T, θ::T, p::Parameters) where {T<:Real}
    dx = x - p.R * cos(θ)
    dy = y - p.R * sin(θ)
    return p.M * hypot(dx, dy)
end

function integral(θ::T, p::Parameters) where {T<:Real}
    domain = (zeros(T, 2), ones(T, 2))
    int_params = (θ = θ, params = p)
    prob = IntegralProblem(integrand, domain, int_params)
    return solve(prob, HCubatureJL()).u
end

params = Parameters(1, 0.5)
v = [1.0, 2.0]
nt = NamedTuple{(:θ, :params)}((π / 2.0, params))

@show integrand(v, nt)
@show gradient(Reverse, integrand, Const(v), nt)
@show integral(π / 2, params)
@show gradient(Reverse, integral, Const(π / 2), params)

Gives this output:

integrand(v, nt) = 1.8027756377319946
gradient(Reverse, integrand, Const(v), nt) = (nothing, (θ = 0.2773500981126146, params = Parameters(0, -0.8320502943378437)))
integral(π / 2, params) = 0.5932334172988466
ERROR: LoadError: Enzyme cannot deduce type
Current scope: 
; Function Attrs: mustprogress willreturn
define internal fastcc void @preprocess_julia_percolate_up__6390({} addrspace(10)* noundef nonnull align 16 dereferenceable(40) "enzyme_type"="{[-1]:Pointer, [-1,0]:Pointer, [-1,0,0]:Float@double, [-1,0,8]:Float@double, [-1,0,16]:Float@double, [-1,0,24]:Float@double, [-1,0,32]:Float@double, [-1,0,40]:Float@double, [-1,0,48]:Integer, [-1,0,49]:Integer, [-1,0,50]:Integer, [-1,0,51]:Integer, [-1,0,52]:Integer, [-1,0,53]:Integer, [-1,0,54]:Integer, [-1,0,55]:Integer, [-1,8]:Integer, [-1,9]:Integer, [-1,10]:Integer, [-1,11]:Integer, [-1,12]:Integer, [-1,13]:Integer, [-1,14]:Integer, [-1,15]:Integer, [-1,16]:Integer, [-1,17]:Integer, [-1,18]:Integer, [-1,19]:Integer, [-1,20]:Integer, [-1,21]:Integer, [-1,22]:Integer, [-1,23]:Integer, [-1,24]:Integer, [-1,25]:Integer, [-1,26]:Integer, [-1,27]:Integer, [-1,28]:Integer, [-1,29]:Integer, [-1,30]:Integer, [-1,31]:Integer, [-1,32]:Integer, [-1,33]:Integer, [-1,34]:Integer, [-1,35]:Integer, [-1,36]:Integer, [-1,37]:Integer, [-1,38]:Integer, [-1,39]:Integer}" "enzymejl_parmtype"="132858765003024" "enzymejl_parmtype_ref"="2" %0, i64 signext "enzyme_inactive" "enzyme_type"="{[-1]:Integer}" "enzymejl_parmtype"="132860064576544" "enzymejl_parmtype_ref"="0" %1, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(11)* nocapture noundef nonnull readonly align 8 dereferenceable(56) "enzyme_type"="{[-1]:Pointer, [-1,0]:Float@double, [-1,8]:Float@double, [-1,16]:Float@double, [-1,24]:Float@double, [-1,32]:Float@double, [-1,40]:Float@double, [-1,48]:Integer, [-1,49]:Integer, [-1,50]:Integer, [-1,51]:Integer, [-1,52]:Integer, [-1,53]:Integer, [-1,54]:Integer, [-1,55]:Integer}" "enzymejl_parmtype"="132858764659024" "enzymejl_parmtype_ref"="1" %2) unnamed_addr #64 !dbg !7143 {
top:
  %3 = call {}*** @julia.get_pgcstack() #65
  %ptls_field26 = getelementptr inbounds {}**, {}*** %3, i64 2
  %4 = bitcast {}*** %ptls_field26 to i64***
  %ptls_load2728 = load i64**, i64*** %4, align 8, !tbaa !40
  %5 = getelementptr inbounds i64*, i64** %ptls_load2728, i64 2
  %safepoint = load i64*, i64** %5, align 8, !tbaa !44
  fence syncscope("singlethread") seq_cst
  call void @julia.safepoint(i64* %safepoint) #65, !dbg !7144
  fence syncscope("singlethread") seq_cst
  %6 = icmp slt i64 %1, 2, !dbg !7145
  br i1 %6, label %L37, label %L6.lr.ph, !dbg !7147

L6.lr.ph:                                         ; preds = %top
  %7 = addrspacecast {} addrspace(10)* %0 to { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)*
  %8 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(11)* %2, i64 0, i32 3
  br label %L6, !dbg !7147

L6:                                               ; preds = %L33, %L6.lr.ph
  %iv = phi i64 [ %iv.next, %L33 ], [ 0, %L6.lr.ph ]
  %value_phi34 = phi i64 [ %1, %L6.lr.ph ], [ %9, %L33 ]
  %iv.next = add nuw nsw i64 %iv, 1, !dbg !7148
  %9 = sdiv i64 %value_phi34, 2, !dbg !7148
  %10 = add nsw i64 %9, -1, !dbg !7150
  %arrayptr29 = load { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)*, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)* %7, align 16, !dbg !7150, !tbaa !85, !alias.scope !7152, !noalias !78, !nonnull !0, !enzyme_type !2145, !enzymejl_byref_BITS_VALUE !0, !enzymejl_source_type_Ptr\7BHCubature.Box\7B2\2C\20Float64\2C\20Float64\2C\20Float64\7D\7D !0
  %11 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr29, i64 %10, !dbg !7150
  %arrayref.sroa.0.0..sroa_cast = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %11 to i8 addrspace(13)*, !dbg !7150
  %arrayref.sroa.1.0..sroa_idx18 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr29, i64 %10, i32 3, !dbg !7150
  %arrayref.sroa.1.0.copyload = load double, double addrspace(13)* %arrayref.sroa.1.0..sroa_idx18, align 1, !dbg !7150, !tbaa !277, !alias.scope !278, !noalias !7155
  %12 = fcmp ord double %arrayref.sroa.1.0.copyload, 0.000000e+00, !dbg !7156
  br i1 %12, label %L12, label %L37.loopexit, !dbg !7158

L12:                                              ; preds = %L6
  %unbox13 = load double, double addrspace(11)* %8, align 8, !dbg !7156, !tbaa !44, !alias.scope !47, !noalias !50, !enzyme_type !94, !enzymejl_byref_BITS_VALUE !0, !enzymejl_source_type_Float64 !0
  %13 = fcmp ord double %unbox13, 0.000000e+00, !dbg !7156
  br i1 %13, label %L32, label %L33, !dbg !7158

L32:                                              ; preds = %L12
  %bitcast_coercion = bitcast double %arrayref.sroa.1.0.copyload to i64, !dbg !7162
  %14 = xor i64 %bitcast_coercion, 9223372036854775807, !dbg !7165
  %15 = icmp slt i64 %bitcast_coercion, 0, !dbg !7167
  %16 = select i1 %15, i64 %14, i64 %bitcast_coercion, !dbg !7167
  %bitcast_coercion16 = bitcast double %unbox13 to i64, !dbg !7162
  %17 = xor i64 %bitcast_coercion16, 9223372036854775807, !dbg !7165
  %18 = icmp slt i64 %bitcast_coercion16, 0, !dbg !7167
  %19 = select i1 %18, i64 %17, i64 %bitcast_coercion16, !dbg !7167
  %20 = icmp slt i64 %16, %19, !dbg !7168
  br i1 %20, label %L33, label %L37.loopexit, !dbg !7151

L33:                                              ; preds = %L32, %L12
  %21 = add nsw i64 %value_phi34, -1, !dbg !7169
  %22 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr29, i64 %21, !dbg !7169
  %23 = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %22 to i8 addrspace(13)*, !dbg !7169
  call void @llvm.memmove.p13i8.p13i8.i64(i8 addrspace(13)* noundef align 8 dereferenceable(56) %23, i8 addrspace(13)* noundef align 1 dereferenceable(56) %arrayref.sroa.0.0..sroa_cast, i64 56, i1 false) #65, !dbg !7169
  %24 = icmp slt i64 %value_phi34, 4, !dbg !7145
  br i1 %24, label %L37.loopexit, label %L6, !dbg !7147

L37.loopexit:                                     ; preds = %L6, %L32, %L33
  %value_phi.lcssa.ph = phi i64 [ %value_phi34, %L6 ], [ %9, %L33 ], [ %value_phi34, %L32 ]
  br label %L37, !dbg !7171

L37:                                              ; preds = %L37.loopexit, %top
  %value_phi.lcssa = phi i64 [ %1, %top ], [ %value_phi.lcssa.ph, %L37.loopexit ]
  %.pre-phi24 = addrspacecast {} addrspace(10)* %0 to { i8 addrspace(13)*, i64, i16, i16, i32 } addrspace(11)*, !dbg !7171
  %25 = add i64 %value_phi.lcssa, -1, !dbg !7171
  %arraylen_ptr = getelementptr inbounds { i8 addrspace(13)*, i64, i16, i16, i32 }, { i8 addrspace(13)*, i64, i16, i16, i32 } addrspace(11)* %.pre-phi24, i64 0, i32 1, !dbg !7171
  %arraylen = load i64, i64 addrspace(11)* %arraylen_ptr, align 8, !dbg !7171, !tbaa !73, !range !76, !alias.scope !77, !noalias !78, !enzyme_type !79, !enzymejl_source_type_UInt64 !0, !enzymejl_byref_BITS_VALUE !0, !enzyme_inactive !0
  %inbounds = icmp ult i64 %25, %arraylen, !dbg !7171
  br i1 %inbounds, label %idxend, label %oob, !dbg !7171

oob:                                              ; preds = %L37
  %errorbox = alloca i64, align 8, !dbg !7171
  store i64 %value_phi.lcssa, i64* %errorbox, align 8, !dbg !7171, !noalias !7173
  %26 = addrspacecast {} addrspace(10)* %0 to {} addrspace(12)*, !dbg !7171
  call void @ijl_bounds_error_ints({} addrspace(12)* %26, i64* nonnull align 8 %errorbox, i64 1) #65, !dbg !7171
  unreachable, !dbg !7171

idxend:                                           ; preds = %L37
  %27 = addrspacecast {} addrspace(10)* %0 to { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)*, !dbg !7171
  %arrayptr1230 = load { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)*, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)* %27, align 16, !dbg !7171, !tbaa !85, !alias.scope !7152, !noalias !78, !nonnull !0, !enzyme_type !2145, !enzymejl_byref_BITS_VALUE !0, !enzymejl_source_type_Ptr\7BHCubature.Box\7B2\2C\20Float64\2C\20Float64\2C\20Float64\7D\7D !0
  %28 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr1230, i64 %25, !dbg !7171
  %29 = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %28 to i8 addrspace(13)*, !dbg !7171
  %30 = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(11)* %2 to i8 addrspace(11)*, !dbg !7171
  call void @llvm.memcpy.p13i8.p11i8.i64(i8 addrspace(13)* noundef align 8 dereferenceable(56) %29, i8 addrspace(11)* noundef nonnull align 8 dereferenceable(56) %30, i64 56, i1 false) #65, !dbg !7171, !tbaa !277, !alias.scope !2495, !noalias !7174
  ret void, !dbg !7171
}

 Type analysis state: 
<analysis>
  %safepoint = load i64*, i64** %5, align 8, !tbaa !37: {}, intvals: {}
  %21 = add nsw i64 %value_phi34, -1, !dbg !104: {[-1]:Integer}, intvals: {}
  %23 = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %22 to i8 addrspace(13)*, !dbg !104: {[-1]:Pointer}, intvals: {}
  %24 = icmp slt i64 %value_phi34, 4, !dbg !40: {[-1]:Integer}, intvals: {}
double 0.000000e+00: {[-1]:Anything}, intvals: {}
  %.pre-phi24 = addrspacecast {} addrspace(10)* %0 to { i8 addrspace(13)*, i64, i16, i16, i32 } addrspace(11)*, !dbg !108: {[-1]:Pointer, [-1,0]:Pointer, [-1,0,0]:Float@double, [-1,0,8]:Float@double, [-1,0,16]:Float@double, [-1,0,24]:Float@double, [-1,0,32]:Float@double, [-1,0,40]:Float@double, [-1,0,48]:Integer, [-1,0,49]:Integer, [-1,0,50]:Integer, [-1,0,51]:Integer, [-1,0,52]:Integer, [-1,0,53]:Integer, [-1,0,54]:Integer, [-1,0,55]:Integer, [-1,0,88]:Float@double, [-1,0,96]:Float@double, [-1,0,144]:Float@double, [-1,0,152]:Float@double, [-1,0,200]:Float@double, [-1,0,208]:Float@double, [-1,0,256]:Float@double, [-1,0,264]:Float@double, [-1,0,312]:Float@double, [-1,0,320]:Float@double, [-1,0,368]:Float@double, [-1,0,376]:Float@double, [-1,0,424]:Float@double, [-1,0,432]:Float@double, [-1,0,480]:Float@double, [-1,0,488]:Float@double, [-1,8]:Integer, [-1,9]:Integer, [-1,10]:Integer, [-1,11]:Integer, [-1,12]:Integer, [-1,13]:Integer, [-1,14]:Integer, [-1,15]:Integer, [-1,16]:Integer, [-1,17]:Integer, [-1,18]:Integer, [-1,19]:Integer, [-1,20]:Integer, [-1,21]:Integer, [-1,22]:Integer, [-1,23]:Integer, [-1,24]:Integer, [-1,25]:Integer, [-1,26]:Integer, [-1,27]:Integer, [-1,28]:Integer, [-1,29]:Integer, [-1,30]:Integer, [-1,31]:Integer, [-1,32]:Integer, [-1,33]:Integer, [-1,34]:Integer, [-1,35]:Integer, [-1,36]:Integer, [-1,37]:Integer, [-1,38]:Integer, [-1,39]:Integer}, intvals: {}
  %25 = add i64 %value_phi.lcssa, -1, !dbg !108: {[-1]:Integer}, intvals: {}
  %arraylen = load i64, i64 addrspace(11)* %arraylen_ptr, align 8, !dbg !108, !tbaa !110, !range !112, !alias.scope !113, !noalias !63, !enzyme_type !114, !enzymejl_source_type_UInt64 !0, !enzymejl_byref_BITS_VALUE !0, !enzyme_inactive !0: {[-1]:Integer}, intvals: {}
  %inbounds = icmp ult i64 %25, %arraylen, !dbg !108: {[-1]:Integer}, intvals: {}
i64 4: {[-1]:Integer}, intvals: {4,}
  %9 = sdiv i64 %value_phi34, 2, !dbg !47: {[-1]:Integer}, intvals: {}
  %10 = add nsw i64 %9, -1, !dbg !51: {[-1]:Integer}, intvals: {}
  %19 = select i1 %18, i64 %17, i64 %bitcast_coercion16, !dbg !100: {}, intvals: {}
  %20 = icmp slt i64 %16, %19, !dbg !102: {[-1]:Integer}, intvals: {}
  %6 = icmp slt i64 %1, 2, !dbg !40: {[-1]:Integer}, intvals: {}
  %ptls_load2728 = load i64**, i64*** %4, align 8, !tbaa !33: {[-1]:Pointer}, intvals: {}
  %13 = fcmp ord double %unbox13, 0.000000e+00, !dbg !75: {[-1]:Integer}, intvals: {}
  %bitcast_coercion = bitcast double %arrayref.sroa.1.0.copyload to i64, !dbg !92: {}, intvals: {}
  %14 = xor i64 %bitcast_coercion, 9223372036854775807, !dbg !97: {}, intvals: {}
i64 -1: {[-1]:Anything}, intvals: {-1,}
  %ptls_field26 = getelementptr inbounds {}**, {}*** %3, i64 2: {[-1]:Pointer, [-1,0]:Pointer}, intvals: {}
i64 2: {[-1]:Integer}, intvals: {2,}
  %8 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(11)* %2, i64 0, i32 3: {[-1]:Pointer, [-1,0]:Float@double}, intvals: {}
  %arrayref.sroa.1.0..sroa_idx18 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr29, i64 %10, i32 3, !dbg !51: {[-1]:Pointer}, intvals: {}
  %7 = addrspacecast {} addrspace(10)* %0 to { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)*: {[-1]:Pointer, [-1,0]:Pointer, [-1,0,0]:Float@double, [-1,0,8]:Float@double, [-1,0,16]:Float@double, [-1,0,24]:Float@double, [-1,0,32]:Float@double, [-1,0,40]:Float@double, [-1,0,48]:Integer, [-1,0,49]:Integer, [-1,0,50]:Integer, [-1,0,51]:Integer, [-1,0,52]:Integer, [-1,0,53]:Integer, [-1,0,54]:Integer, [-1,0,55]:Integer, [-1,0,88]:Float@double, [-1,0,96]:Float@double, [-1,0,144]:Float@double, [-1,0,152]:Float@double, [-1,0,200]:Float@double, [-1,0,208]:Float@double, [-1,0,256]:Float@double, [-1,0,264]:Float@double, [-1,0,312]:Float@double, [-1,0,320]:Float@double, [-1,0,368]:Float@double, [-1,0,376]:Float@double, [-1,0,424]:Float@double, [-1,0,432]:Float@double, [-1,0,480]:Float@double, [-1,0,488]:Float@double, [-1,8]:Integer, [-1,9]:Integer, [-1,10]:Integer, [-1,11]:Integer, [-1,12]:Integer, [-1,13]:Integer, [-1,14]:Integer, [-1,15]:Integer, [-1,16]:Integer, [-1,17]:Integer, [-1,18]:Integer, [-1,19]:Integer, [-1,20]:Integer, [-1,21]:Integer, [-1,22]:Integer, [-1,23]:Integer, [-1,24]:Integer, [-1,25]:Integer, [-1,26]:Integer, [-1,27]:Integer, [-1,28]:Integer, [-1,29]:Integer, [-1,30]:Integer, [-1,31]:Integer, [-1,32]:Integer, [-1,33]:Integer, [-1,34]:Integer, [-1,35]:Integer, [-1,36]:Integer, [-1,37]:Integer, [-1,38]:Integer, [-1,39]:Integer}, intvals: {}
  %arraylen_ptr = getelementptr inbounds { i8 addrspace(13)*, i64, i16, i16, i32 }, { i8 addrspace(13)*, i64, i16, i16, i32 } addrspace(11)* %.pre-phi24, i64 0, i32 1, !dbg !108: {[-1]:Pointer, [-1,0]:Integer, [-1,1]:Integer, [-1,2]:Integer, [-1,3]:Integer, [-1,4]:Integer, [-1,5]:Integer, [-1,6]:Integer, [-1,7]:Integer}, intvals: {}
  %3 = call {}*** @julia.get_pgcstack() #65: {[-1]:Pointer, [-1,16]:Pointer}, intvals: {}
{} addrspace(10)* %0: {[-1]:Pointer, [-1,0]:Pointer, [-1,0,0]:Float@double, [-1,0,8]:Float@double, [-1,0,16]:Float@double, [-1,0,24]:Float@double, [-1,0,32]:Float@double, [-1,0,40]:Float@double, [-1,0,48]:Integer, [-1,0,49]:Integer, [-1,0,50]:Integer, [-1,0,51]:Integer, [-1,0,52]:Integer, [-1,0,53]:Integer, [-1,0,54]:Integer, [-1,0,55]:Integer, [-1,0,88]:Float@double, [-1,0,96]:Float@double, [-1,0,144]:Float@double, [-1,0,152]:Float@double, [-1,0,200]:Float@double, [-1,0,208]:Float@double, [-1,0,256]:Float@double, [-1,0,264]:Float@double, [-1,0,312]:Float@double, [-1,0,320]:Float@double, [-1,0,368]:Float@double, [-1,0,376]:Float@double, [-1,0,424]:Float@double, [-1,0,432]:Float@double, [-1,0,480]:Float@double, [-1,0,488]:Float@double, [-1,8]:Integer, [-1,9]:Integer, [-1,10]:Integer, [-1,11]:Integer, [-1,12]:Integer, [-1,13]:Integer, [-1,14]:Integer, [-1,15]:Integer, [-1,16]:Integer, [-1,17]:Integer, [-1,18]:Integer, [-1,19]:Integer, [-1,20]:Integer, [-1,21]:Integer, [-1,22]:Integer, [-1,23]:Integer, [-1,24]:Integer, [-1,25]:Integer, [-1,26]:Integer, [-1,27]:Integer, [-1,28]:Integer, [-1,29]:Integer, [-1,30]:Integer, [-1,31]:Integer, [-1,32]:Integer, [-1,33]:Integer, [-1,34]:Integer, [-1,35]:Integer, [-1,36]:Integer, [-1,37]:Integer, [-1,38]:Integer, [-1,39]:Integer}, intvals: {}
i64 %1: {[-1]:Integer}, intvals: {}
{ [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(11)* %2: {[-1]:Pointer, [-1,0]:Float@double, [-1,8]:Float@double, [-1,16]:Float@double, [-1,24]:Float@double, [-1,32]:Float@double, [-1,40]:Float@double, [-1,48]:Integer, [-1,49]:Integer, [-1,50]:Integer, [-1,51]:Integer, [-1,52]:Integer, [-1,53]:Integer, [-1,54]:Integer, [-1,55]:Integer}, intvals: {}
  %iv = phi i64 [ %iv.next, %L33 ], [ 0, %L6.lr.ph ]: {[-1]:Integer}, intvals: {0,}
i64 9223372036854775807: {[-1]:Anything}, intvals: {9223372036854775807,}
  %15 = icmp slt i64 %bitcast_coercion, 0, !dbg !100: {[-1]:Integer}, intvals: {}
  %bitcast_coercion16 = bitcast double %unbox13 to i64, !dbg !92: {[-1]:Float@double}, intvals: {}
  %17 = xor i64 %bitcast_coercion16, 9223372036854775807, !dbg !97: {}, intvals: {}
  %18 = icmp slt i64 %bitcast_coercion16, 0, !dbg !100: {[-1]:Integer}, intvals: {}
  %4 = bitcast {}*** %ptls_field26 to i64***: {[-1]:Pointer, [-1,0]:Pointer}, intvals: {}
  %iv.next = add nuw nsw i64 %iv, 1, !dbg !47: {[-1]:Integer}, intvals: {1,}
  %12 = fcmp ord double %arrayref.sroa.1.0.copyload, 0.000000e+00, !dbg !75: {[-1]:Integer}, intvals: {}
  %errorbox = alloca i64, align 8, !dbg !108: {}, intvals: {}
  %26 = addrspacecast {} addrspace(10)* %0 to {} addrspace(12)*, !dbg !108: {}, intvals: {}
  %27 = addrspacecast {} addrspace(10)* %0 to { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)*, !dbg !108: {[-1]:Pointer, [-1,0]:Pointer, [-1,0,0]:Float@double, [-1,0,8]:Float@double, [-1,0,16]:Float@double, [-1,0,24]:Float@double, [-1,0,32]:Float@double, [-1,0,40]:Float@double, [-1,0,48]:Integer, [-1,0,49]:Integer, [-1,0,50]:Integer, [-1,0,51]:Integer, [-1,0,52]:Integer, [-1,0,53]:Integer, [-1,0,54]:Integer, [-1,0,55]:Integer, [-1,0,88]:Float@double, [-1,0,96]:Float@double, [-1,0,144]:Float@double, [-1,0,152]:Float@double, [-1,0,200]:Float@double, [-1,0,208]:Float@double, [-1,0,256]:Float@double, [-1,0,264]:Float@double, [-1,0,312]:Float@double, [-1,0,320]:Float@double, [-1,0,368]:Float@double, [-1,0,376]:Float@double, [-1,0,424]:Float@double, [-1,0,432]:Float@double, [-1,0,480]:Float@double, [-1,0,488]:Float@double, [-1,8]:Integer, [-1,9]:Integer, [-1,10]:Integer, [-1,11]:Integer, [-1,12]:Integer, [-1,13]:Integer, [-1,14]:Integer, [-1,15]:Integer, [-1,16]:Integer, [-1,17]:Integer, [-1,18]:Integer, [-1,19]:Integer, [-1,20]:Integer, [-1,21]:Integer, [-1,22]:Integer, [-1,23]:Integer, [-1,24]:Integer, [-1,25]:Integer, [-1,26]:Integer, [-1,27]:Integer, [-1,28]:Integer, [-1,29]:Integer, [-1,30]:Integer, [-1,31]:Integer, [-1,32]:Integer, [-1,33]:Integer, [-1,34]:Integer, [-1,35]:Integer, [-1,36]:Integer, [-1,37]:Integer, [-1,38]:Integer, [-1,39]:Integer}, intvals: {}
  %value_phi.lcssa = phi i64 [ %1, %top ], [ %value_phi.lcssa.ph, %L37.loopexit ]: {[-1]:Integer}, intvals: {}
i64 1: {[-1]:Integer}, intvals: {1,}
  %arrayptr1230 = load { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)*, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)* %27, align 16, !dbg !108, !tbaa !55, !alias.scope !58, !noalias !63, !nonnull !0, !enzyme_type !68, !enzymejl_byref_BITS_VALUE !0, !enzymejl_source_type_Ptr\7BHCubature.Box\7B2\2C\20Float64\2C\20Float64\2C\20Float64\7D\7D !0: {[-1]:Pointer, [-1,0]:Float@double, [-1,8]:Float@double, [-1,16]:Float@double, [-1,24]:Float@double, [-1,32]:Float@double, [-1,40]:Float@double, [-1,48]:Integer, [-1,49]:Integer, [-1,50]:Integer, [-1,51]:Integer, [-1,52]:Integer, [-1,53]:Integer, [-1,54]:Integer, [-1,55]:Integer, [-1,88]:Float@double, [-1,96]:Float@double, [-1,144]:Float@double, [-1,152]:Float@double, [-1,200]:Float@double, [-1,208]:Float@double, [-1,256]:Float@double, [-1,264]:Float@double, [-1,312]:Float@double, [-1,320]:Float@double, [-1,368]:Float@double, [-1,376]:Float@double, [-1,424]:Float@double, [-1,432]:Float@double, [-1,480]:Float@double, [-1,488]:Float@double}, intvals: {}
  %unbox13 = load double, double addrspace(11)* %8, align 8, !dbg !75, !tbaa !37, !alias.scope !89, !noalias !90, !enzyme_type !91, !enzymejl_byref_BITS_VALUE !0, !enzymejl_source_type_Float64 !0: {[-1]:Float@double}, intvals: {}
  %value_phi34 = phi i64 [ %1, %L6.lr.ph ], [ %9, %L33 ]: {[-1]:Integer}, intvals: {}
  %28 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr1230, i64 %25, !dbg !108: {[-1]:Pointer, [-1,0]:Float@double, [-1,8]:Float@double, [-1,16]:Float@double, [-1,24]:Float@double, [-1,32]:Float@double, [-1,40]:Float@double, [-1,48]:Integer, [-1,49]:Integer, [-1,50]:Integer, [-1,51]:Integer, [-1,52]:Integer, [-1,53]:Integer, [-1,54]:Integer, [-1,55]:Integer}, intvals: {}
  %16 = select i1 %15, i64 %14, i64 %bitcast_coercion, !dbg !100: {}, intvals: {}
  %22 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr29, i64 %21, !dbg !104: {[-1]:Pointer}, intvals: {}
  %11 = getelementptr inbounds { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 }, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %arrayptr29, i64 %10, !dbg !51: {[-1]:Pointer}, intvals: {}
  %arrayref.sroa.0.0..sroa_cast = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %11 to i8 addrspace(13)*, !dbg !51: {[-1]:Pointer}, intvals: {}
  %29 = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* %28 to i8 addrspace(13)*, !dbg !108: {[-1]:Pointer, [-1,0]:Float@double, [-1,8]:Float@double, [-1,16]:Float@double, [-1,24]:Float@double, [-1,32]:Float@double, [-1,40]:Float@double, [-1,48]:Integer, [-1,49]:Integer, [-1,50]:Integer, [-1,51]:Integer, [-1,52]:Integer, [-1,53]:Integer, [-1,54]:Integer, [-1,55]:Integer}, intvals: {}
  %arrayref.sroa.1.0.copyload = load double, double addrspace(13)* %arrayref.sroa.1.0..sroa_idx18, align 1, !dbg !51, !tbaa !72, !alias.scope !73, !noalias !74: {}, intvals: {}
  %5 = getelementptr inbounds i64*, i64** %ptls_load2728, i64 2: {[-1]:Pointer}, intvals: {}
  %30 = bitcast { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(11)* %2 to i8 addrspace(11)*, !dbg !108: {[-1]:Pointer, [-1,0]:Float@double, [-1,8]:Float@double, [-1,16]:Float@double, [-1,24]:Float@double, [-1,32]:Float@double, [-1,40]:Float@double, [-1,48]:Integer, [-1,49]:Integer, [-1,50]:Integer, [-1,51]:Integer, [-1,52]:Integer, [-1,53]:Integer, [-1,54]:Integer, [-1,55]:Integer}, intvals: {}
  %value_phi.lcssa.ph = phi i64 [ %value_phi34, %L6 ], [ %9, %L33 ], [ %value_phi34, %L32 ]: {[-1]:Integer}, intvals: {}
  %arrayptr29 = load { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)*, { [1 x [2 x double]], [1 x [2 x double]], double, double, i64 } addrspace(13)* addrspace(11)* %7, align 16, !dbg !51, !tbaa !55, !alias.scope !58, !noalias !63, !nonnull !0, !enzyme_type !68, !enzymejl_byref_BITS_VALUE !0, !enzymejl_source_type_Ptr\7BHCubature.Box\7B2\2C\20Float64\2C\20Float64\2C\20Float64\7D\7D !0: {[-1]:Pointer, [-1,0]:Float@double, [-1,8]:Float@double, [-1,16]:Float@double, [-1,24]:Float@double, [-1,32]:Float@double, [-1,40]:Float@double, [-1,48]:Integer, [-1,49]:Integer, [-1,50]:Integer, [-1,51]:Integer, [-1,52]:Integer, [-1,53]:Integer, [-1,54]:Integer, [-1,55]:Integer, [-1,88]:Float@double, [-1,96]:Float@double, [-1,144]:Float@double, [-1,152]:Float@double, [-1,200]:Float@double, [-1,208]:Float@double, [-1,256]:Float@double, [-1,264]:Float@double, [-1,312]:Float@double, [-1,320]:Float@double, [-1,368]:Float@double, [-1,376]:Float@double, [-1,424]:Float@double, [-1,432]:Float@double, [-1,480]:Float@double, [-1,488]:Float@double}, intvals: {}
i64 0: {[-1]:Anything}, intvals: {0,}
</analysis>

Cannot deduce type of copy   call void @llvm.memmove.p13i8.p13i8.i64(i8 addrspace(13)* noundef align 8 dereferenceable(56) %23, i8 addrspace(13)* noundef align 1 dereferenceable(56) %arrayref.sroa.0.0..sroa_cast, i64 56, i1 false) #65, !dbg !104

Caused by:
Stacktrace:
 [1] setindex!
   @ ./array.jl:1021
 [2] percolate_up!
   @ ~/.julia/packages/DataStructures/95DJa/src/heaps/arrays_as_heaps.jl:39
within MethodInstance for DataStructures.percolate_up!(::Vector{HCubature.Box{2, Float64, Float64, Float64}}, ::Int64, ::HCubature.Box{2, Float64, Float64, Float64}, ::Base.Order.ReverseOrdering{Base.Order.ForwardOrdering})


Stacktrace:
  [1] setindex!
    @ ./array.jl:1021 [inlined]
  [2] percolate_up!
    @ ~/.julia/packages/DataStructures/95DJa/src/heaps/arrays_as_heaps.jl:39
  [3] percolate_up!
    @ ~/.julia/packages/DataStructures/95DJa/src/heaps/arrays_as_heaps.jl:48 [inlined]
  [4] heappush!
    @ ~/.julia/packages/DataStructures/95DJa/src/heaps/arrays_as_heaps.jl:73 [inlined]
  [5] push!
    @ ~/.julia/packages/DataStructures/95DJa/src/heaps/binary_heap.jl:91 [inlined]
  [6] hcubature_
    @ ~/.julia/packages/HCubature/rqE0Y/src/HCubature.jl:156 [inlined]
  [7] hcubature_
    @ ~/.julia/packages/HCubature/rqE0Y/src/HCubature.jl:0 [inlined]
  [8] augmented_julia_hcubature__6376_inner_1wrap
    @ ~/.julia/packages/HCubature/rqE0Y/src/HCubature.jl:0
  [9] macro expansion
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:5445 [inlined]
 [10] enzyme_call
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4983 [inlined]
 [11] AugmentedForwardThunk
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4919 [inlined]
 [12] runtime_generic_augfwd(activity::Type{…}, runtimeActivity::Val{…}, width::Val{…}, ModifiedBetween::Val{…}, RT::Val{…}, f::typeof(HCubature.hcubature_), df::Nothing, primal_1::Integrals.var"#72#74"{…}, shadow_1_1::Base.RefValue{…}, primal_2::StaticArraysCore.SVector{…}, shadow_2_1::Base.RefValue{…}, primal_3::StaticArraysCore.SVector{…}, shadow_3_1::Base.RefValue{…}, primal_4::typeof(LinearAlgebra.norm), shadow_4_1::Nothing, primal_5::Float64, shadow_5_1::Base.RefValue{…}, primal_6::Float64, shadow_6_1::Base.RefValue{…}, primal_7::Int64, shadow_7_1::Nothing, primal_8::Int64, shadow_8_1::Nothing, primal_9::Nothing, shadow_9_1::Nothing)
    @ Enzyme.Compiler ~/.julia/packages/Enzyme/g1jMR/src/rules/jitrules.jl:424
 [13] hcubature_
    @ ~/.julia/packages/HCubature/rqE0Y/src/HCubature.jl:179 [inlined]
 [14] augmented_julia_hcubature__5002wrap
    @ ~/.julia/packages/HCubature/rqE0Y/src/HCubature.jl:0
 [15] macro expansion
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:5445 [inlined]
 [16] enzyme_call
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4983 [inlined]
 [17] AugmentedForwardThunk
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4919 [inlined]
 [18] runtime_generic_augfwd(activity::Type{…}, runtimeActivity::Val{…}, width::Val{…}, ModifiedBetween::Val{…}, RT::Val{…}, f::typeof(HCubature.hcubature_), df::Nothing, primal_1::Integrals.var"#72#74"{…}, shadow_1_1::Base.RefValue{…}, primal_2::Vector{…}, shadow_2_1::Vector{…}, primal_3::Vector{…}, shadow_3_1::Vector{…}, primal_4::typeof(LinearAlgebra.norm), shadow_4_1::Nothing, primal_5::Float64, shadow_5_1::Base.RefValue{…}, primal_6::Float64, shadow_6_1::Base.RefValue{…}, primal_7::Int64, shadow_7_1::Nothing, primal_8::Int64, shadow_8_1::Nothing, primal_9::Nothing, shadow_9_1::Nothing)
    @ Enzyme.Compiler ~/.julia/packages/Enzyme/g1jMR/src/rules/jitrules.jl:424
 [19] hcubature
    @ ~/.julia/packages/HCubature/rqE0Y/src/HCubature.jl:234 [inlined]
 [20] #__solvebp_call#70
    @ ~/.julia/packages/Integrals/e3NH3/src/Integrals.jl:217
 [21] __solvebp_call
    @ ~/.julia/packages/Integrals/e3NH3/src/Integrals.jl:190 [inlined]
 [22] __solvebp
    @ ~/.julia/packages/Integrals/e3NH3/src/Integrals.jl:71 [inlined]
 [23] __solve
    @ ~/.julia/packages/Integrals/e3NH3/src/Integrals.jl:69 [inlined]
 [24] #__solve#52
    @ ~/.julia/packages/Integrals/e3NH3/src/Integrals.jl:92
 [25] __solve
    @ ~/.julia/packages/Integrals/e3NH3/src/Integrals.jl:79 [inlined]
 [26] solve!
    @ ~/.julia/packages/Integrals/e3NH3/src/common.jl:108 [inlined]
 [27] #solve#3
    @ ~/.julia/packages/Integrals/e3NH3/src/common.jl:104 [inlined]
 [28] solve
    @ ~/.julia/packages/Integrals/e3NH3/src/common.jl:101 [inlined]
 [29] solve
    @ ~/.julia/packages/Integrals/e3NH3/src/common.jl:0 [inlined]
 [30] augmented_julia_solve_6260_inner_1wrap
    @ ~/.julia/packages/Integrals/e3NH3/src/common.jl:0
 [31] macro expansion
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:5445 [inlined]
 [32] enzyme_call
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4983 [inlined]
 [33] AugmentedForwardThunk
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4919 [inlined]
 [34] runtime_generic_augfwd(activity::Type{…}, runtimeActivity::Val{…}, width::Val{…}, ModifiedBetween::Val{…}, RT::Val{…}, f::typeof(solve), df::Nothing, primal_1::IntegralProblem{…}, shadow_1_1::Base.RefValue{…}, primal_2::HCubatureJL{…}, shadow_2_1::Nothing)
    @ Enzyme.Compiler ~/.julia/packages/Enzyme/g1jMR/src/rules/jitrules.jl:424
 [35] integral
    @ ~/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:26 [inlined]
 [36] integral
    @ ~/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:0 [inlined]
 [37] augmented_julia_integral_6230_inner_1wrap
    @ ~/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:0
 [38] macro expansion
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:5445 [inlined]
 [39] enzyme_call
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4983 [inlined]
 [40] AugmentedForwardThunk
    @ ~/.julia/packages/Enzyme/g1jMR/src/compiler.jl:4919 [inlined]
 [41] autodiff
    @ ~/.julia/packages/Enzyme/g1jMR/src/Enzyme.jl:396 [inlined]
 [42] autodiff
    @ ~/.julia/packages/Enzyme/g1jMR/src/Enzyme.jl:524 [inlined]
 [43] macro expansion
    @ ~/.julia/packages/Enzyme/g1jMR/src/sugar.jl:275 [inlined]
 [44] gradient(rm::ReverseMode{false, false, FFIABI, false, false}, f::typeof(integral), x::Const{Float64}, args::Parameters)
    @ Enzyme ~/.julia/packages/Enzyme/g1jMR/src/sugar.jl:262
 [45] macro expansion
    @ show.jl:1181 [inlined]
 [46] top-level scope
    @ ~/software/julia/bugs/enzyme-integrals-namedtuple/enzyme-bug.jl:36
 [47] include(fname::String)
    @ Base.MainInclude ./client.jl:494
 [48] top-level scope
    @ REPL[6]:1

This would require a custom Enzyme rule in Integrals.jl that no one has implemented yet. In general, reverse mode autodiff over adaptive quadrature isn’t widely available, neither in Julia nor elsewhere as far as I know. However, Integrals.jl does provide a rule for Zygote/ChainRules, so maybe try Zygote?

Got it. Thanks for that explanation.

btw there’s work in progress to make QuadGK.jl work seamlessly with Enzyme. But QuadGK is only for 1D integrals, so unfortunately that won’t help your use case.

Oh, and lastly, it shouldn’t be too difficult to write an Enzyme rule for your specific case. The math is straightforward. The kind of completely general rule that Integrals.jl itself would need to implement is trickier because it needs to handle arbitrarily nested structures of parameters captured within the integrand closure, but writing your own rule for your specific problem is a lot easier because you only need to handle the particular, known structure you’re working with. So that’s something you could try. I’d take a crack at this myself if I had the time, but I’m unable to right now.

Great. Thanks for that suggestion. Earlier, I skimmed the Zygote rule that I believe you referenced. It looks doable, but for expediency, I’ll just use a simple sum for my integral for now.

Did you try using Zygote instead of Enzyme to differentiate your code as-is? If the rule in Integrals.jl works, that shouldn’t require anything extra. Haven’t tried it myself, though.

I tried Zygote and got an expected error because of mutability (we have written our code with Enzyme in mind because there are cases where we want mutability). Even refactoring that erroring function to be pure, I have another error whose effort for a solution is probably on par with writing a custom rule for the quadrature rule (ERROR: LoadError: Need an adjoint for constructor). Ideally, I would write each of those rules, but at this point it’s just a proof of principal. In the future, I’ll take a crack at it.

1 Like