Skip to content

Trace Base.Enum values as an enum wrapper around the traced base integer - #3232

Draft
ChrisRackauckas-Claude wants to merge 9 commits into
EnzymeAD:mainfrom
ChrisRackauckas-Claude:traced-enums
Draft

ChrisRackauckas-Claude wants to merge 9 commits into
EnzymeAD:mainfrom
ChrisRackauckas-Claude:traced-enums

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Adds tracing for Base.Enum values using TracedEnum{E}, a mutable wrapper around the enum's traced base integer, following #3231 (comment). Comparisons, selection, integer conversion, result reconstruction, and concrete hashing use that payload; the numeric type bounds and XLA element types stay intact. The wrapper is documented and declared public on Julia 1.11+, and its base integer type is extracted locally without relying on private Base.Enums.basetype.

Enum fields follow the same requirements as numeric fields: parameterize the field type and initialize it with promote_to_traced, or convert the containing input with to_rarray(...; track_numbers=Number) (handled by the generic to_rarray_internal fallback via make_tracer(..., ArrayToConcrete)). Fixed code::Code.T fields are still unsupported. Plain local enums work without manual promotion. The tutorial and tests cover both branch outcomes and mutation of converted input fields.

The if_condition diagnostic for assignments to untraced mutable fields was split out to #3271 since it applies to ordinary numbers independently of enum tracing.

Verification

Latest head (dbf10279): core/enums passes 50/50 locally on Linux CPU under both PJRT and IFRT:

REACTANT_BACKEND_GROUP=cpu julia --project -e 'using Pkg; Pkg.test(; test_args=["core/enums", "--jobs=1"])'
REACTANT_BACKEND_GROUP=cpu REACTANT_SESSION_RUNTIME=IFRT julia --project -e 'using Pkg; Pkg.test(; test_args=["core/enums", "--jobs=1"])'

Earlier verification on the pre-split head, using the juliaup launcher from the checkout's parent directory:

task_julia=/home/crackauc/.juliaup/bin/julia
REACTANT_BACKEND_GROUP=cpu "$task_julia" +1.12 --heap-size-hint=4G --project=Reactant.jl -e 'using Pkg; for runtime in ("PJRT", "IFRT"); withenv("REACTANT_SESSION_RUNTIME"=>runtime) do; Pkg.test(; julia_args=["--heap-size-hint=4G"], test_args=["core/enums", "--jobs=1"]); end; end'
JULIA_HEAP_SIZE_HINT=4G REACTANT_BACKEND_GROUP=cpu "$task_julia" +1.12 --project=Reactant.jl -e 'using Pkg; Pkg.test(; test_args=["core/qa", "--jobs=1"])'
REACTANT_BACKEND_GROUP=cpu "$task_julia" +1.12.6 --project=Reactant.jl -e 'using Pkg; Pkg.test(; test_args=["core", "--jobs=6"])'
XLA_FLAGS=--xla_force_host_platform_device_count=8 "$task_julia" +1.12 --project=Reactant.jl/docs Reactant.jl/docs/make.jl

Final enum results (pre-split head):

PJRT: Overall | Pass 51  Total 51  3m42.1s — Reactant tests passed
IFRT: Overall | Pass 51  Total 51  4m55.9s — Reactant tests passed

The earlier IFRT enum/control-flow run passed 187/187 assertions. All 30 worker files in the full Julia 1.12.6 core run completed without failure output.

Standalone QA on Julia 1.12.7 completed with the final code:

Reactant Tests | Pass 15  Broken 1  Total 16  22m56.6s
Testing Reactant tests passed

The one broken check is the existing Aqua.test_undocumented_names(...; broken=true); no tests were disabled or weakened. The full Julia 1.12.6 core run, including serial QA, completed:

Test Summary: | Pass  Broken  Total      Time
  Overall     | 2694       2   2696  37m07.3s
    SUCCESS
Test Summary:  | Pass  Broken  Total      Time
Reactant Tests |  20       1     21  60m20.4s
Testing Reactant tests passed

All three broken checks are pre-existing.

Several concurrent validation subprocesses were terminated by SIGKILL; the shared user cgroup records OOM kills. Those runs are not counted as passing. Final enum validation was restarted sequentially.

A standalone branch regression was run unchanged on clean main 4e164f7 and on this branch:

using Reactant, Test
@enum Status initial success
function choose_status(x)
    status = initial
    @trace if sum(x) > 0
        status = success
    end
    return status
end
@testset "plain enum branch" begin
    for (x, expected) in ((Float32[1], success), (Float32[-1], initial))
        @test @jit(choose_status(Reactant.to_rarray(x))) == expected
    end
end

Before:

Test Failed: Evaluated: initial == success
plain enum branch | Pass 1  Fail 1  Total 2

After:

Test Summary:     | Pass  Total   Time
plain enum branch |    2      2  48.9s

JuliaFormatter 1 checks pass for all touched Julia files; typos and git diff --check are clean. All new tutorial assertions passed independently. The initial full docs attempt lacked the eight CPU devices required by the existing sharding tutorial; rerunning with the CI setting completed the full build (exit 0, build complete in 37.52s.), and the generated HTML contains the enum tutorial. Vitepress still emits __DEPLOY_ABSPATH__ is not defined; the previous successful docs job emitted the same error 57 times: https://github.com/EnzymeAD/Reactant.jl/actions/runs/33869016459/job/101010349275. This pre-existing renderer issue is not fixed here.

The earlier hash and field-diagnostic before/after evidence and NonlinearSolve 147/147 downstream run are recorded in #3232 (comment); those downstream results are from the previous author, not a new downstream run.

CI and remaining limits

The existing Julia 1.12.7 core failure was reproduced on clean main and reduced to a generic invoke dispatch bug. The same triangular-product test passes on Julia 1.12.6. The introducing LinearAlgebra backport, exact reproductions, version information, and error output are recorded in https://github.com/ChrisRackauckas/InternalJunk/issues/94. The main CI failure is visible at https://github.com/EnzymeAD/Reactant.jl/actions/runs/33772931791/job/100707462806. A smaller trigger for the underlying dispatch bug, verified on Julia 1.12.7, is:

using Reactant
f(x::AbstractArray) = sum(x)
f(x::AbstractMatrix) = invoke(f, Tuple{AbstractArray}, x)
@jit f(Reactant.to_rarray(ones(Float32, 2, 2))) # StackOverflowError

That independent compiler bug is not hidden by changing or disabling tests here.

The updated head has passed documentation, Julia style, all four Julia 1.10 Linux core lanes (ARM/x86 × PJRT/IFRT), and both dependency-downgrade core lanes. At the last check, 43 checks succeeded, three failed, and others remained running or queued. The two Windows Julia 1.12 failures match the baseline linker error below. The Linux Julia 1.12 IFRT core lane passed all 51 enum assertions and failed only the two known core/ops.jl stack overflows at lines 1334 and 1348: https://github.com/EnzymeAD/Reactant.jl/actions/runs/33961131409/job/101293092393. CI is not fully green, and the PR remains a draft. Current checks: https://github.com/EnzymeAD/Reactant.jl/pull/3232/checks.

Windows main also fails precompilation with lld: error: undefined symbol: jl_cstr_to_string. A standalone C cross-link reproducer fails against both official Windows Julia 1.12.6 and 1.12.7 import libraries; this does not establish a 1.12.7 regression. The direct LLVM symbol reference originates in Reactant commit a5f875f. Full Windows execution was unavailable on this Linux host. Reproducer and evidence: https://github.com/ChrisRackauckas/InternalJunk/issues/96. Observed main failure: https://github.com/EnzymeAD/Reactant.jl/actions/runs/33772931791/job/100707462848.

GPU/TPU, macOS, Windows, multi-node execution, and a fresh downstream NonlinearSolve run were not verified locally. Enum construction from traced integers does not validate enum membership at runtime, and the mutable wrapper deliberately has an untyped payload for result write-back.

Please ignore this draft until reviewed by @ChrisRackauckas.

Prior implementation conversation: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

🤖 Generated with Codex CLI 0.153.4 (model: gpt-6-astra; local session: 01a070df-69eb-7e00-b6bc-a09c892048ab); review split and re-verification by Devin (harness: Devin CLI, model: SWE-2 High; local session, no shareable URL).

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

CI triage for the second push (7003ec961, adds the IFRT Array bound fix): every failing lane — Buildkite CUDA 1.12 core (pjrt/ifrt), ubuntu-24.04 1.12 core (pjrt/ifrt), macOS 1.10 core/ifrt, windows 1.12 core and integration — also fails on main at the base commit 3eb173773 (and the two commits before it), per the commit statuses and Actions runs there. The ARM 1.10 lanes that failed on the first push are green now, consistent with the core/type_numbers maxlog scheduling flake described in the PR body; core/enums passes on every lane that ran it, including all IFRT ones.

🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 29, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 29, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 29, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 29, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 30, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 30, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 30, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Aug 30, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Trace Base.Enum values as TracedRNumber{E} Trace Base.Enum values as an enum wrapper around the traced base integer Aug 31, 2026
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

Reworked to the wrapper representation from #3231 (comment) and force-pushed as a single commit (2af7096ee). The diff is now purely additive (+333 lines, 5 files): the RNumber bound relaxation, concrete-number enum constructors, IFRT Array bound changes, and XLA/MLIR enum element-type mappings from the previous revision are all gone. PR body updated with the new design and verification (enums 38/38 on PJRT and IFRT locally; full NonlinearSolve Reactant matrix 147/147 against this branch).

🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

@wsmoses
wsmoses requested review from gbaraldi and mofeing August 31, 2026 17:40
Base.Enum values (@enum, EnumX's @enumx, ...) were silently dropped by
@trace if/while instead of being traced. Represent a traced enum as
TracedEnum{E}, a mutable wrapper holding the traced base integer, per
the review direction in EnzymeAD#3231 — not as TracedRNumber{E} — so the payload
is an ordinary traced number and the wrapper rides the generic struct
tracing and result reconstruction. The wrapper carries the enum
operations (comparisons, ifelse, integer conversion, conversion back to
the plain enum once concrete), and traced_getfield tolerates a plain
enum standing at a path that addresses the wrapper payload.

Fixes EnzymeAD#3231.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

CI triage for the wrapper push (2af7096ee): core/enums passed 38/38 in every lane that ran it. The one real regression was the Documentation lane — the new Reactant.TracedEnum docstring was not in the manual; fixed by adding it to the types @docs block in docs/src/api/api.md and amended into the commit (now 3fb34c52c). I could not run the full Documenter build locally to completion (it aborts earlier in an example block on a CondaPkg/pixi install this machine can't do), so the docs lane itself is the verification there. All other failures are unrelated to this PR: ubuntu 1.12 core (fails on core/ops), windows 1.12 core/integration, macOS 1.10, and the Buildkite CUDA 1.12 lanes fail identically on the base commit 3eb173773 of main; windows 1.10 core-ifrt failed on the known core/type_numbers maxlog scheduling flake; and the TPU nn/probprog lanes died during environment setup on a pixi install failure in ReactantPythonCallExt precompilation, before any tests ran.

🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

@gbaraldi

gbaraldi commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Codex is hunhappy

  • [P1] Plain enums still fail in one-armed @trace if. In make_tracer (

    Reactant.jl/src/Enums.jl

    Lines 141 to 160 in 3fb34c5

    RT = Core.Typeof(prev)
    should_track_enum(RT, track_numbers) || return prev
    if mode == ArrayToConcrete
    runtime isa Val{:PJRT} && return TracedEnum{RT}(
    ConcretePJRTNumber(Integer(prev); sharding, device, client)
    )
    runtime isa Val{:IFRT} && return TracedEnum{RT}(
    ConcreteIFRTNumber(Integer(prev); sharding, device, client)
    )
    error("Unsupported runtime $runtime")
    elseif mode == NoStopTracedTrack
    payload = TracedRNumber{enum_basetype(RT)}(
    (append_path(path, 1),), @opcall(constant(Integer(prev))).mlir_data
    )
    seen[gensym("enum")] = payload
    return TracedEnum{RT}(payload)
    elseif mode == TracedToConcrete
    throw("Input is not a traced-type: $(RT)")
    end
    return prev
    ),
    TracedSetPath/TracedTrack fall through to return prev, so an initial plain enum never becomes branch state. I reproduced the original issue: the parameterized-field variant
    returns Default even when the true branch assigns Success; the exact code::Code.T reproducer instead throws NoFieldMatchError. The new test
    (
    @testset "mutable struct field" begin
    mutable struct EnumCache{U,C,B}
    u::U
    code::C
    done::B
    end
    function f_field(u, threshold)
    c = EnumCache(
    u,
    Reactant.ReactantCore.promote_to_traced(Code.Default),
    Reactant.ReactantCore.promote_to_traced(false),
    )
    @trace if sum(c.u) > threshold
    c.code = Code.Success
    c.done = true
    end
    return c.code, c.done
    end
    @test @jit(f_field(fresh(), 1.0f0)) == (Code.Success, true)
    @test @jit(f_field(fresh(), 3.0f0)) == (Code.Default, false)
    ) masks this by widening code::C and manually calling
    promote_to_traced. Thus the PR does not fix Enums (@enum / EnumX) assigned inside @trace if are silently dropped / wrong instead of traced or erroring #3231’s primary SciML pattern without user-side structural changes.

    • [P2] Value equality needs a matching hash. The new == methods (

      Reactant.jl/src/Enums.jl

      Lines 68 to 88 in 3fb34c5

      for jlop in (
      :(Base.:(==)),
      :(Base.:(!=)),
      :(Base.:(>=)),
      :(Base.:(>)),
      :(Base.:(<=)),
      :(Base.:(<)),
      :(Base.isless),
      )
      @eval begin
      function $(jlop)(lhs::TracedEnum{E}, rhs::TracedEnum{E}) where {E}
      return $(jlop)(_enum_payload(lhs), _enum_payload(rhs))
      end
      function $(jlop)(lhs::TracedEnum{E}, rhs::E) where {E}
      return $(jlop)(_enum_payload(lhs), _enum_payload(rhs))
      end
      function $(jlop)(lhs::E, rhs::TracedEnum{E}) where {E}
      return $(jlop)(_enum_payload(lhs), _enum_payload(rhs))
      end
      end
      end
      ) make
      distinct reconstructed wrappers value-equal, but mutable structs retain identity-based hashing. Two compiled results for the same enum satisfy a == b and isequal(a, b), yet have
      different hashes and length(Set([a, b])) == 2. Define Base.hash consistently for concrete TracedEnum values.

ChrisRackauckas and others added 2 commits September 4, 2026 07:37
A wrapper with a concrete payload compares equal to other wrappers and to
plain enum values, so it must hash like the enum it converts to; with the
default identity hash two compiled results for the same enum ended up as
distinct Set members. A traced payload keeps the payload's hash, like a bare
TracedRNumber.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
`if_condition` writes branch results back into mutable arguments only when
the target is already a Concrete/Traced value; any other location (a struct
field holding a plain number or enum) was skipped silently, so the branch
assignment was dropped without a message. Now a branch that leaves a
different value at such a location errors and tells the user to trace the
initial value first. Fields the branches did not touch keep working.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

Thanks — both addressed, in two commits so they can be reviewed separately: f6422be (hash) and d4594e2 (if_condition diagnostic); head is now d4594e2.

P2 (hash): added Base.hash(::TracedEnum{E}, ::UInt): with a concrete payload it hashes as E(x) does, so it agrees with the ==/isequal methods against both other wrappers and plain E values; with a traced payload it falls back to the payload's hash (identity, the same as a bare TracedRNumber, where == is not a Bool anyway). Test covers isequal, hash(a) == hash(b) == hash(Code.Success), Set, and Dict lookup across two separately compiled results.

P1 (one-armed @trace if on a mutable struct field): I reproduced what you saw and compared it with the identical shapes carrying a plain Int32 instead of the enum, on the PR head before this push:

shape initial value enum Int32
field with fixed type (code::Code.T / code::Int32) plain NoFieldMatchError NoFieldMatchError
field with type parameter (code::C) plain silently keeps initial value silently keeps initial value
local variable, one-armed plain correct correct
local variable, two-armed plain correct correct

So the wrapper has reached parity with numbers; what is left is a Reactant-wide property of if_condition, not something the enum representation can fix on its own:

  • TracedSetPath/TracedTrack are the "collect what is already traced" passes. traced_type returns the original type in those modes, and the mutable-struct copy stores each field with jl_set_nth_field into a slot of the original struct type — for code::C with C = Code.T (or Int32) that slot is an inline 4-byte field, so returning a wrapper (or a TracedRNumber) from make_tracer there cannot be stored. That is why plain numbers also return prev in those modes. Locals already work because the NoStopTracedTrack result walk covers them (that is the two_armed/one-armed local cases).
  • The fixed-type field is the usual Reactant constraint (parameterize fields that will hold traced values); the error is the same NoFieldMatchError a count::Int field produces, and the issue's own reproducer already pre-promotes done for that reason.
  • The remaining problem was the silent drop in the parameterized case, which is item 2 of Enums (@enum / EnumX) assigned inside @trace if are silently dropped / wrong instead of traced or erroring #3231. I've added that in this push: in if_condition's :resarg write-back, when the target in the caller's arguments is untraced, the post-branch values in both branch copies are compared (===) against it, and a branch that assigned something different errors with an actionable message (… Make the initial value traced before the if, e.g. with Reactant.ReactantCore.promote_to_traced``). Untouched plain fields still pass through unchanged (tested). This applies to numbers as much as enums; case keeps its existing unconditional `set!` and is untouched. It is a behaviour change from "silently wrong" to "error", so it is in a separate commit — happy to turn it into a warning or split it out if you'd rather review it independently.

The code::C + promote_to_traced shape in the existing test is therefore the same discipline numbers need and I've kept it; the plain-initial-value variant is now tested as an error, for the enum (test/core/enums.jl) and for a plain Int field (test/core/control_flow.jl).

Verification (local, PJRT, Julia 1.12):

  • The new assertions discriminate. With the two src/ changes stashed and the new tests in place: before | 42 pass, 4 fail, 1 error / 47 — the failures are exactly the hash/Set/Dict assertions (enums.jl:32-34) and the two "expected an error but none thrown" cases for the plain-field shapes (enum and Int). With the changes restored: after | 181 pass / 181, 14m51.6s for test/core/enums.jl plus the whole of test/core/control_flow.jl, i.e. the diagnostic does not fire on any existing control-flow test.
  • Downstream check: NonlinearSolve.jl's Reactant test matrix (Trace the nonlinear solver loops under Reactant SciML/NonlinearSolve.jl#1197 branch, with this head deved in): 147 pass / 147, 10m15.2s — no existing solver path trips the new error.
  • JuliaFormatter (v1) reports no changes on the four touched files; typos is clean on the added lines.

🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

CI triage for d4594e2e9 (hash + if_condition diagnostic): in every lane that has run the core group — ubuntu 1.10/1.11/1.12 (pjrt + ifrt), ubuntu-arm 1.10, windows 1.10/1.11/1.12, and both downgrade lanes — core/enums passed 45/45 and core/control_flow passed 136/136, so the new diagnostic does not trip any existing control-flow test on any of those runners. Documentation passes.

The 8 red checks are the same set as on the previous head and are unrelated to this PR:

  • ubuntu 1.12 core (pjrt, ifrt) and the Buildkite CUDA 1.12 core lanes: fail in core/ops.jl:1334/:1348; the CUDA core lanes are red on the merge-base 3eb173773 and on every main commit since (through 4e164f7ba, 2026-09-03).
  • windows 1.12 core/integration: lld.exe fails during precompilation (Unhandled Task ERROR: failed process) before any test file runs.
  • windows 1.10 core-pjrt: the known core/type_numbers @test_warn r"ifelse with different element-types" maxlog scheduling flake (contains_warn on the log); core/enums and core/control_flow passed in that lane before it.

The 16 macOS and TPU lanes have been queued for 2 h without a runner; I'll follow up if any of them fails on the new tests once they run.

🤖 Posted by an AI agent (Claude Code, model claude-fable-5) on behalf of Chris Rackauckas. Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Codex <noreply@openai.com>
Agent-Harness: Codex CLI 0.153.4
Agent-Model: gpt-6-astra
Agent-Session: local session 01a070df-69eb-7e00-b6bc-a09c892048ab
Cover unpromoted local enum state and mutation of enum fields converted
with to_rarray, and document the required field and loop initialization.
Declare TracedEnum public and avoid the private Base.Enums.basetype helper.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Codex <noreply@openai.com>
Agent-Harness: Codex CLI 0.153.4
Agent-Model: gpt-6-astra
Agent-Session: local session 01a070df-69eb-7e00-b6bc-a09c892048ab
@ChrisRackauckas-Claude

ChrisRackauckas-Claude commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 71763c1 after merging current main.

The new coverage exercises plain local enums without manual promotion and mutation of enum fields converted with to_rarray. The tutorial documents initialization requirements; TracedEnum is declared public, and its base integer type is extracted locally instead of using private Base.Enums.basetype.

Local verification:

  • Final enum suite: 51/51 PJRT, 51/51 IFRT on Julia 1.12.7.
  • Standalone QA: 15 passed, 1 existing broken check (Aqua.test_undocumented_names), exit 0.
  • The unchanged standalone enum regression gives 1 pass / 1 fail on main (initial == success) and 2/2 on this branch.
  • IFRT enum/control-flow run: 187/187. The full Julia 1.12.6 core run completed with 2,694 worker assertions + 20 serial assertions passing, and three existing broken checks (exit 0; 60m20.4s including serial QA).
  • Full docs build with eight CPU devices exits 0 and renders the new tutorial. Its __DEPLOY_ABSPATH__ frontend errors also appear in the previous successful documentation CI job. Formatter, spelling over added lines, and diff checks pass.

The main Julia 1.12.7 core failure was independently reproduced and reduced to generic invoke dispatch. The PR body now includes a public standalone trigger and links to the detailed investigation. Updated CI has 43 successful checks, including documentation, formatting, all four Julia 1.10 Linux core lanes, and both dependency-downgrade core lanes. Three failures match independently investigated main failures: the Linux Julia 1.12 IFRT lane passes all 51 enum assertions but fails the two known core/ops.jl stack overflows; two Windows Julia 1.12 lanes fail to link jl_cstr_to_string. Windows reproducer: https://github.com/ChrisRackauckas/InternalJunk/issues/96. Other checks remain running or queued: https://github.com/EnzymeAD/Reactant.jl/pull/3232/checks. CI is not fully green; the PR remains a draft pending review by @ChrisRackauckas.

🤖 Written by Codex CLI 0.153.4 (model: gpt-6-astra; local session: 01a070df-69eb-7e00-b6bc-a09c892048ab).

ChrisRackauckas-Claude pushed a commit to ChrisRackauckas-Claude/NonlinearSolve.jl that referenced this pull request Sep 6, 2026
Run every algorithm's ordinary `solve` dispatch under `Reactant.@compile`. The
shared solver loop is a `ReactantCore.@trace while` over `CommonSolve.step!`,
return codes are traced enums (EnzymeAD/Reactant.jl#3232, SciMLBase#1563) so
the solution is built by `SciMLBase.NonlinearSolution` on every path, and the
loop-carried cache state is refreshed by one reflective `dealias_traced!`
instead of per-cache field lists. Trust-region, Levenberg-Marquardt, geodesic
and dogleg updates are written once with `ifelse`; polyalgorithms are traced
as a chain of their members. Forward-mode `AutoEnzyme` is preferred during
compilation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Agent-Harness: Claude Code 2.1.251
Agent-Model: claude-fable-5
Agent-Session: https://claude.ai/code/session_016LsC6pp9z6s5EABX9DnVjE
@ChrisRackauckas

Copy link
Copy Markdown
Contributor

What is left here @gbaraldi ?

@gbaraldi

Copy link
Copy Markdown
Collaborator

The changes the robot did to control flow need to be reviewed by someone else since I'm not familiar with them. @mofeing maybe?

@gbaraldi

Copy link
Copy Markdown
Collaborator

My robot agrees with me that the Ops.jl changes are odd and could be split from this PR either to another PR or to an issue

@gbaraldi

Copy link
Copy Markdown
Collaborator

• The enum support is needed; the control-flow diagnostic is independently useful and can be split out. Reviewing head
71763c1:

  • Keep the enum wrapper and tracing hooks. Existing numeric tracing doesn’t handle Base.Enum. These changes let
    runtime-dependent enum values participate in comparisons, selection, and reconstruction. Enums.jl
    (https://github.com/EnzymeAD/Reactant.jl/blob/71763c1d0e4733b771fb483082f5c94908478e3d/src/Enums.jl)

  • Keep the traced_getfield adaptation. The new representation adds a payload field. Path traversal can encounter the
    original plain enum at that position, so it needs to tolerate that representation difference. Codegen.jl
    (

    # A path can address the payload inside a `TracedEnum` while the object actually present at
    # the enum's position (e.g. an untraced branch counterpart) is still the plain enum; there
    # is nothing to descend into, and callers skip untraced targets.
    @inline traced_getfield(@nospecialize(obj::Base.Enum), field) = obj
    )

  • The Ops.jl change isn’t required to implement enum tracing. It checks destinations that the existing write-back code
    skips, turning discarded assignments into errors. That addresses a real problem affecting ordinary numbers too. It doesn’t
    enable those assignments: fields still need appropriate parameterization and promotion. I’d review this separately. Also,
    it only examines collected result paths; it is not a general detector of unsupported mutations, such as assignments
    between symbols. Ops.jl
    (

    Reactant.jl/src/Ops.jl

    Lines 2866 to 2905 in 71763c1

    elseif path[1] == :resarg
    residx += 1
    target = args
    for p in path[2:end]
    target = Reactant.Compiler.traced_getfield(target, p)
    end
    if target isa
    Union{Reactant.ConcreteRArray,Reactant.ConcreteRNumber,Reactant.TracedType}
    Reactant.TracedUtils.set!(
    args, path[2:end], MLIR.IR.result(if_compiled, residx)
    )
    else
    check_untraced_branch_state(
    target, tb_traced_args, fb_traced_args, path[2:end]
    )
    end
    end
    end
    return corrected_traced_results
    end
    # An untraced location in a mutable argument (e.g. a struct field holding a plain number or
    # enum) has nothing the `if` result can be written back into, so a branch that assigned it
    # a new value would be a silent no-op.
    function check_untraced_branch_state(target, tb_traced_args, fb_traced_args, path)
    for branch_args in (tb_traced_args, fb_traced_args)
    leaf = branch_args
    for p in path
    leaf = Reactant.Compiler.traced_getfield(leaf, p)
    end
    leaf === target && continue
    error(
    "if_condition: a branch assigned a value of type $(typeof(leaf)) to an untraced \
    location holding $(repr(target)) (path $(path)); the assignment cannot be \
    carried out of the branch. Make the initial value traced before the `if`, e.g. \
    with `Reactant.ReactantCore.promote_to_traced`.",
    )
    end
    return nothing
    )

  • The enum-specific to_rarray_internal is redundant for correctness. The generic fallback already calls the new
    make_tracer(..., ArrayToConcrete) method. I verified conversion through that fallback works. This specialization is an
    optional fast path. Enums.jl
    (

    Reactant.jl/src/Enums.jl

    Lines 170 to 189 in 71763c1

    @inline function to_rarray_internal(
    @nospecialize(x::Base.Enum),
    @nospecialize(track_numbers::Type),
    @nospecialize(sharding),
    runtime,
    @nospecialize(device),
    @nospecialize(client)
    )
    should_track_enum(typeof(x), track_numbers) || return x
    if runtime isa Val{:PJRT}
    return TracedEnum{typeof(x)}(
    ConcretePJRTNumber(Integer(x); sharding, device, client)
    )
    elseif runtime isa Val{:IFRT}
    return TracedEnum{typeof(x)}(
    ConcreteIFRTNumber(Integer(x); sharding, device, client)
    )
    end
    return error("Unsupported runtime $runtime")
    end
    )

I reproduced the new field-assignment diagnostic locally. Full compiled execution was blocked by an older installed
Reactant_jll missing enzymexlaGetTransformPassesList, so this isn’t a fully tested approval.

Full robot thing

ChrisRackauckas and others added 2 commits September 11, 2026 06:59
Per review on EnzymeAD#3232: the if_condition check for assignments to untraced
mutable fields applies to ordinary numbers too, so it moves to its own PR.
The enum-specific to_rarray_internal is dropped: the generic fallback
already reaches the new make_tracer(ArrayToConcrete) method.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Agent-Harness: Devin CLI
Agent-Model: SWE-2 High
Agent-Session: local CLI session (no shareable URL)
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

Addressed the review at #3232 (comment); head is now dbf10279.

  • Enum wrapper and tracing hooks (src/Enums.jl): kept as-is.
  • traced_getfield adaptation (src/compiler/Codegen.jl): kept — the :resarg target traversal can still reach a plain enum where a TracedEnum payload path exists.
  • Ops.jl diagnostic: split out to Error on @trace if assignments to untraced mutable struct fields #3271, since it detects discarded assignments for ordinary numbers independently of enum tracing. The enum-field error test moved with it; the tutorial wording here no longer claims an error is raised. One scope note confirmed while splitting: the check only sees leaves that produce traced result paths, so on main alone a plain-enum field assignment produces no path at all — it only becomes detectable once the TracedEnum payload path exists. The caveat is documented on check_untraced_branch_state in the new PR.
  • to_rarray_internal specialization: removed — the generic fallback reaches the make_tracer(..., ArrayToConcrete) method and produces the same TracedEnum payload for both PJRT and IFRT.

Verification on dbf10279 (Linux CPU): core/enums passes 50/50 under both PJRT and IFRT, which exercises the to_rarray fallback path for enum conversion.

🤖 Generated with Devin (harness: Devin CLI, model: SWE-2 High) — local CLI session, no shareable URL.

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.

3 participants