Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions lib/ModelingToolkitBase/src/systems/codegen_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,16 @@ function eval_or_rgf(expr::Expr; eval_expression = false, eval_module = @__MODUL
if eval_expression
return eval_module.eval(expr)
else
return drop_expr(RuntimeGeneratedFunction(eval_module, eval_module, expr))
# Only function-definition expressions benefit from RuntimeGeneratedFunction (avoids
# world-age issues with new methods). Module-level references such as
# `:(ModelingToolkitBase._oop_unimplemented)` are not function definitions; wrapping
# them in an RGF would fail. Evaluate them directly instead — no new method is
# introduced so there is no world-age concern.
if Meta.isexpr(expr, :function) || Meta.isexpr(expr, :->)
return drop_expr(RuntimeGeneratedFunction(eval_module, eval_module, expr))
else
return eval_module.eval(expr)
end
end
end

Expand Down Expand Up @@ -595,12 +604,36 @@ Base.@nospecializeinfer function build_function_wrapper(
end

optimize = resolve_optimize_option(optimize)
return Symbolics.codegen_function(ir, expr, args; wrap_code, similarto, cse, optimize, kwargs...)
result = Symbolics.codegen_function(ir, expr, args; wrap_code, similarto, cse, optimize, kwargs...)
# When iip_config disables one side, Symbolics generates an anonymous `unimplemented`
# function expression. Replace it here — where we know which side is disabled — with a
# reference to a named module-level function so all downstream paths (expression=Val{true},
# eval_expression=true, distributed serialization) get a stable, serializable callable.
iip_config = get(kwargs, :iip_config, (true, true))
if result isa NTuple{2, Expr}
oop_expr, iip_expr = result
if !iip_config[1]
oop_expr = OOP_UNIMPLEMENTED_EXPR
end
if !iip_config[2]
iip_expr = IIP_UNIMPLEMENTED_EXPR
end
result = (oop_expr, iip_expr)
end
return result
end

resolve_optimize_option(x) = x
resolve_optimize_option(::Nothing) = nothing

# Module-level fallback functions for the disabled side of an `iip_config` pair.
# Using named module-level functions ensures correct serialization across all codegen paths
# (expression=Val{true} evaluated by the user, eval_expression=true, distributed workers).
_oop_unimplemented(args...) = throw(Symbolics.FunctionUnimplementedError("out-of-place"))
_iip_unimplemented(args...) = throw(Symbolics.FunctionUnimplementedError("in-place"))
const OOP_UNIMPLEMENTED_EXPR = :($ModelingToolkitBase._oop_unimplemented)
const IIP_UNIMPLEMENTED_EXPR = :($ModelingToolkitBase._iip_unimplemented)

"""
$(TYPEDEF)

Expand Down
16 changes: 16 additions & 0 deletions lib/ModelingToolkitBase/test/odesystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ f.f(du, u, p, 0.1)
@test du == [4, 0, -16]
@test_throws Symbolics.FunctionUnimplementedError f.f(u, p, 0.1)

# check that iip_config with expression = Val{true} produces serializable functions
# (regression test for https://github.com/SciML/ModelingToolkit.jl/issues/4464)
f_iip_expr = ODEFunction(de; iip_config = (false, true), expression = Val{true})
f_iip_from_expr = eval(f_iip_expr)
# verify the OOP stub is the stable module-level function (not a Main closure)
@test f_iip_from_expr.f.f_oop === ModelingToolkitBase._oop_unimplemented
# verify round-trip serialization works (simulates distributed usage)
@testset "Issue#4464" begin
using Serialization
buf = IOBuffer()
serialize(buf, f_iip_from_expr)
seekstart(buf)
f2 = deserialize(buf)
@test f2.f.f_oop === ModelingToolkitBase._oop_unimplemented
end

#check iip
f = eval(ODEFunction(de; expression = Val{true}))
f2 = ODEFunction(de)
Expand Down
Loading