Module allocatables as run state, character values and dummies, stub-dropped reads, use only-lists: what ELM's PhotosynthesisMod forced - #34
Open
chenyueqi wants to merge 32 commits into
Conversation
… is a value; a character scalar dummy is flat Four rules an ELM unit forced, none of them about ELM: * flatten: a module allocatable's shape is its own module's ALLOCATE (`allocate (vcmax_np1 (0:mxpft))`), its run-time extent spelled by the module variables that size it, which become recorded scalars; the Fortran adapter allocates over the original bounds before assigning and the recorder takes such an extent from size(). A reader that use-imports the array subscripts it over the companion's lower bound, not the blanket one-based shift (the slatop(0:mxpft) class, for module arrays). * use-constants: a character parameter (`namep = 'pft'`) builds as a str node carrying the value, rendered by repr for Python, never folded through integer division. * a character scalar dummy (`phase = 'sun'`) is a flat argument: declared character(len=*) on the Fortran side, written as text by the recorder, read back as text by the replay parser, admitted by the bit-exact gate under the recorded convention. * the recorder's own argument is recast_phase, so it cannot collide with a dummy named phase; the allocate emitter no longer crashes on a conflicting-bounds record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…and flushes after every section A pointer component the configuration never allocated is not read by the routine either, but the probe reading it is a fault. Each component or state the plan found an ALLOCATE for is written under associated() / allocated(), and recorded as poison of the planned shape otherwise (NaN for reals, zero and false where there is no NaN), so the replay has a value to hand over and any use of it shows. The unit is flushed after each section so a crash leaves the sections before it on disk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…ument reads are named to the read/write gate; reshape is an intrinsic * Companions carry the only-list the frontend saw (`only`), and the translation registers as companion globals only the names it lets in: under `use pftvarcon, only: vcmax_np1, ...`, ELM's Photosynthesis associates c3psn, qe, theta_cj, slatop while pftvarcon exports arrays of the same names, and the reader resolved the aliases to the wrong module -- writing photosyns_vars%qe_patch into pftvarcon.qe. * A statement stub drops its call's arguments: the transform names the reads it dropped per block (dropped_reads), the protocol carries them, and static.rwset excuses exactly those the target does not read, counting them on the verdict (reads_excused_by_stubs). A block that is nothing but an abort (a raise with the stub marker) is waived by name, which only a pass used to be. * reshape, spread, pack, unpack, transpose, cshift, eoshift, lbound are transformational intrinsics, not variables a block reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…n the evidence of a gate run maxloc and minloc join reshape and the rest; the vocabulary test that recorded the deliberate divergence now records why it ended: ELM's PhotosynthesisMod readParams, 93 blocks matching with them counted as intrinsics. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…one; a call's positional actuals follow the emitted convention
ELM's hybrid/brent/ci_func: `if (f0 == 0) return` ahead of the loop,
returning the same tuple of output names at every return -- nothing to
select through jnp.where, the guarded statements after the flag leave the
outputs as they were. And the anchor's call omits OUT scalars
(`hybrid(..., gs_mol, iter, atm2lnd_vars, photosyns_vars)`), so counting
them against the callee's dummies shifted the objects ("object
photosyns_vars not passed"). With both, photosynthesis_flat, hybrid_flat,
brent_flat and ci_func_flat lower to lax kernels, root finders included.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…rget The anchor starts it as None and points it under phase == 'sun' or 'sha'; a lax.cond cannot carry None against an array (ELM's Photosynthesis: fourteen sun/shade views, 'cond branch outputs must have the same pytree structure'). Seeded with the first arm's target -- a pure reference, never a computation -- which the arm the run takes overwrites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
nscaler = 1 where nscaler is real(r8): Fortran converts, the backend typed the bare constant int32, and a lax.cond arm disagreed with the float64 one beside it. The guard init says which strong type the local is; the rewrite casts the literal to it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…bulary spells The NumPy runtime guards math.sqrt so a negative real is a NaN and not a crash; a kernel that inlines quadratic() met the name undefined. jnp.sqrt answers the same. _f_cfold is the compile-time fold the anchor spells for a constant-argument intrinsic; a kernel meets it at trace time only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
hybrid and brent: convergence exits ahead of `if (iter > itmax) exit`; the cap is what makes the loop finite, and a fixed count under the done flag lowers to a scan reverse mode can transpose, where lax.while_loop has no rule. The JAX runtime's mpmath fold is exempted by the contract the way the NumPy runtime's is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
hybrid's loop runs as 'while not _ret' after its early returns merged; the cap read off its 'if (iter > itmax) exit' still makes it a fixed count, the condition joining the done flag in the guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…get it may have psn_z => photosyns_vars%psnsun_z_patch under sun, the shade array under shade, then psn_z(p,iv) = ...: ELM's leaf photosynthesis, stomatal resistance and ci were planned read-only -- an associate alias was followed, a pointer assignment was not -- and the gate never compared the routine's own outputs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…ve pointed at In the anchor psn_z(p,iv) = ... after psn_z => psnsun_z_patch writes the component's own storage; in a kernel it rebinds the local and the flat output it aliased came back as it went in -- d(psn)/d(psn) was exactly one and the ULP gate, once it compared those outputs, read 9e18 ULP. Before the return each target takes the local under the condition of its pointing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…ely pointed The pointer assignment counts as a write of the alias in the read/write sets, so every sun/shade input read through one (par_z, lai_z, vcmaxcint) was planned as an output and echoed unchanged: 48 outputs on Photosynthesis, 42 the call can change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK
…where a dummy of it is declared ELM's params_inst is public and protected while its type, photo_params_type, is private to PhotosynthesisMod. The Fortran adapter and the recorder imported every object's type; the recorder for PhotosynthesisHydraulicStress did not compile (Symbol 'photo_params_type' not found in module). State is use-associated by the variable's name and touched through its components; only a dummy needs its type declared. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
ELM's ColumnType allocates dz(begc:endc, -nlevsno+1:nlevgrnd) in its own init; PhotosynthesisMod subscripts dz over that bound and imports nothing named nlevsno. The bound's names were rendered as the reader would spell them, i.e. bare: NameError at the first call of the hydraulic-stress kernel. A name in a companion's bound is bound to the module that declares it -- the companion itself, the module its USE names, or another companion -- and a module none of the companions is gets imported. Module-level and derived-type component allocations alike. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
Fortran's SUM accumulates element by element; np.sum is pairwise past eight elements and rounds differently. ELM's hydraulic-stress kernel sums its ten soil layers (root conductance, the soil-root flux balance) and the pairwise sum put 925 of 267,264 recorded points up to 75 ULP off the recording; a sequential fold, replayed on the same dumps, put every one of them on it. _f_vsum in both runtimes, as _f_vdot already is; the JAX port's masked-slice rule recognises it; a DIM reduction keeps the library's order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
…the host fails by name The JAX module binds what it could not lower to the NumPy anchor (f = _host.f) and lists what it did lower in _JAX_KERNELS. The tolerance verifier fetched the name from the module and judged the anchor's own code, awarding the port a bit-exact verdict on ELM's hydraulic-stress kernel, which it had never emitted. A name outside the lowered list is not compared: it fails, with the backend's recorded reason, unless declared ungated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
…n chain Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
…all's OUT scalars bind to temporaries; a nested function's return is its own
Three shapes of ELM's PhotosynthesisHydraulicStress chain the port refused.
The positional convention is the anchor's: dummies in declaration order, OUT
scalars omitted, an OUT array passed in its place -- spacf(p, c, x, f, ...).
Appending the buffers after the rest shifted every actual behind one, and
calcstress handed spacF a flux for atm2lnd_inst ("object not passed"). In the
anchor's buffered form (_out = calcstress(...); bsun = _out[1]) an OUT scalar
has no actual: it binds to a temporary the unpack reads, an object's unpack
is dropped (its components bound at the call). A statement function is a
nested def whose return is not an early return of the kernel.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
…hing lowers to nothing PhotosynthesisHydraulicStress carries its statement functions as nested defs at the top of the body: the early-return scan entered them from the root and refused the routine. brent_PHS checks its bracket per phase and ends the run; with the raise dropped the loop carried nothing and the backend refused the kernel. As a guard around nothing, it lowers to nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
The anchor spells PhotosynthesisMod's own params_inst bare (params_inst.psi50 inside plc); the flat spelling knew dummies and companion state only, so every hydraulic-stress helper kernel took params_inst as an opaque parameter that no calling kernel had -- latent while the SP recordings never ran them, a NameError the first time the hydraulic-stress kernel did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
The flat rewrite spells a module's own state object by its components; the record's module_state_read still says params_inst, and a closure taken from it gave plc's kernel an opaque trailing parameter that every calling kernel passed bare -- NameError the first time the hydraulic-stress kernel ran. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
calcstress runs its Newton loop inside if (.not. night); the loop's done flag is assigned in the branch, so the branch carries it, and the lax.cond operand tuple read _done_1 before anything had bound it. Like the goto-region flags, every done flag starts False at the top of the function. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
After an early return or a break, each following statement was wrapped in a guard of its own; a call and the unpack of its result became two lax.conds, and the temporary the first bound was not in the second's carry (NameError _t1 in brent_PHS). Consecutive guarded statements share one block; a statement that returns or breaks closes it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
transpose(a) is np.asfortranarray(a.T) in the anchor, a layout the F-order reshape downstream relies on; jnp has no layouts and no asfortranarray, and SimpleMathMod's kernel raised AttributeError under the hydraulic-stress kernel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
brent_PHS's integer, parameter :: nphs = 2 sizes its bracket arrays; the backend strengthened the literal to jnp.int32(2) for a stable carry dtype and jnp.zeros((nphs,)) refused a traced shape. A shape is a trace-time constant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
The anchor binds a pointer alias twice, as the view it opens the body with and again where the Fortran's => stands. Counted as two stores it was a plain local, rebound in the loops and never written back to the flat output: PhotosynthesisHydraulicStress returned every output as it came in (9e18 ULP). Every top-level store binding the same value is one alias. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
The capped while lowering gave its counter back, so hybrid_PHS's two capped loops were both _done_1 and the inner loop's exit ended the outer after one pass (iter1 stayed 1 where the anchor ran to 4). The counter is a name supply. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
…oops take the flag brent returns from inside its bracketing loop ahead of the next ci_func evaluation. The flag alone left the rest of the pass and the following passes running, and the kernel's gs_mol was an evaluation the Fortran never made (the nutrient-limited recording is the first where hybrid reaches brent). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
A for loop lowers to a scan whose body cannot break (the first form put a break outside any loop in SimpleMathMod's kernel). The loop's body runs under if not _ret, the statements after the flag in any block run under it too, and a while loop takes it in its test: a guarded no-op pass is what a scan can do. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
brent_PHS updates its bracket arrays inside do-phase loops; with the pass under if not _ret those loops sat under an If, where the assigned-names walk stopped, and the while carry lost them (UnboundLocalError: c in the pass). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
… of its base Wrapped whole in an If, a while True was no longer forever and the cap it breaks on was out of the capped lowering's sight: the loop became a while_loop reverse mode cannot transpose, whose carry is computed before lowering, where brent_PHS's c(phase) = ... is a subscript store the walk did not count. The rest of the pass runs under the flag and the test takes it; a for loop, which has no test, keeps the whole-body guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E99K3jFzUL8MmHzwyUPNGz
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rules two ELM units forced, none about ELM; each with a test naming the case. With them,
Photosynthesis(E3SMe508436e45,components/elm/src/biogeophys/PhotosynthesisMod.F90) translates with zero deferred sites, is bit-exact on a recording of the model's own run (61,440 points, 0 ULP, read/write sets matching on 93 blocks), and lowers -- root finders included, as fixed-count loops -- tolaxkernels within 10 ULP (day 1) and 21 ULP dominant (noon) of the same recordings, every output compared, withjax.gradagreeing with forward mode to 8e-15 and forward mode with finite differences of the anchor to 3.5e-7 through the root finders (private extensiona85tract/recast-elm,docs/photosynthesis.md).Plan, adapters, recorder
allocate (vcmax_np1 (0:mxpft))) is the shape, the extent spelled by the module variables that size it, which become recorded scalars; the Fortran adapter allocates over the original bounds; the recorder takes such an extent fromsize(). A reader that use-imports the array subscripts it over the companion's lower bound.namep = 'pft'): astrnode, rendered byrepr.phase = 'sun') is flat:character(len=*)in the adapter, text in the recorder and replay, admitted by the bit-exact gate under the recorded convention.recast_phase; the allocate emitter no longer crashes on a conflicting-bounds record.associated()/allocated()), records NaN of the planned shape otherwise, and flushes after every section.psn_z => photosyns_vars%psnsun_z_patchwrites every target it may have, where the alias is assigned (not merely pointed). Left out, the routine's own outputs (leaf photosynthesis, stomatal resistance, ci) were planned read-only and never compared: the first bit-exact verdict stood on 20 outputs, the present one on 42 (129,024 points).Translation and the read/write gate
7. A
use ..., only:bounds what a companion exports (an associate aliasc3psnresolved topftvarcon.c3psnand wrotephotosyns_vars%qe_patchintopftvarcon.qe).8. A stubbed call's argument reads are named to the read/write gate (
dropped_reads), excused only where the target does not read them; a block that is nothing but an abort is waived by name.9. The array transforms (
reshape,spread,pack,unpack,transpose,cshift,eoshift,maxloc,minloc,lbound) are intrinsics to the analysis; the vocabulary test that kept the divergence records the gate run that ended it.The JAX port
10. A subroutine's early returns of its output names merge on the flag alone (
hybrid'sif (f0 == 0) return).11. A call's positional actuals follow the emitted convention (non-OUT dummies, then OUT array buffers):
hybrid(..., gs_mol, iter, atm2lnd_vars, photosyns_vars)omits the OUT scalars.12. A pointer local pointed in a branch starts the kernel as its first target (fourteen sun/shade views a
lax.condcould not carry asNone).13. An integer literal into a real local is a float (
nscaler = 1).14. The JAX runtime carries
_f_sqrtand_f_cfoldas the NumPy one does (mpmath exempted by the contract the same way).15. A
while Trueits counter caps through abreakis a fixed count of passes under the done flag (and under any loop condition, the single-exit flag included):lax.fori_loopwith static bounds, which reverse mode transposes, wherelax.while_loophas no rule.16. A write through a branch-pointed local reaches every target it may have pointed at:
psn_z(p,iv) = ...rebinds a JAX local, and the flat output it aliased came back as it went in (d(psn)/d(psn) was exactly one; the ULP gate read 9e18 ULP once it compared those outputs). Before the return each target takes the local under the condition of its pointing.What the hydraulic-stress unit forced (
PhotosynthesisHydraulicStress, the same module underuse_hydrstress = .true., recorded on a sibling case: 96 calls, 159 inputs, 50 outputs; bit-exact on 267,264 points in NumPy, and every one of them within 23 ULP as alaxkernel -- the plant-hydraulics Newton solve and both capped root finders as fixed counts of passes)17. A module-state object is reached by its name; its type is named only where a dummy of it is declared.
params_instis public and protected whilephoto_params_typeis private to the module: the recorder'suse PhotosynthesisMod, only: photo_params_typedid not compile.18. A borrowed ALLOCATE bound is spelled in the companion's namespace. ColumnType allocates
dz(begc:endc, -nlevsno+1:nlevgrnd); PhotosynthesisMod subscriptsdzover that bound and imports nothing namednlevsno, so the subscript read a barenlevsno(NameErrorat the first call). The name binds to the module that declares it, imported if the reader has not; module-level and derived-type component allocations alike.19. SUM is a left fold in element order.
np.sumis pairwise past eight elements; over the ten soil layers it put 925 of 267,264 recorded points up to 75 ULP off, and a sequential fold replayed on the same dumps put every one of them on the recording._f_vsumin both runtimes, as_f_vdotalready is; a DIM reduction keeps the library's order.20. The ULP gate judges what the backend lowered; a routine forwarded to the host fails by name. The JAX module binds what it could not lower to the anchor (
f = _host.f) and lists what it did lower in_JAX_KERNELS; the tolerance verifier fetched the name from the module and awarded the port a bit-exact verdict on the hydraulic-stress kernel it had never emitted. A name outside the lowered list is not compared: it fails with the backend's recorded reason unless declared ungated (issue #32, row 22).21. A call's OUT arrays are buffers in their declared place. The anchor's positional convention is declaration order with OUT scalars omitted and an OUT array passed where it stands (
spacf(p, c, x, f, qflx_sun, ...)); the rewrite appended the buffers after the other dummies, shifting every actual behind one, socalcstresshandedspacFa flux foratm2lnd_inst("object not passed" -- a misalignment, not a missing object).22. A buffered call's OUT scalars bind to temporaries. In
_out = calcstress(...); bsun = _out[1]an OUT scalar has no actual to bind to ("output bsun has no target"); it binds to a temporary the unpack reads, and an object's unpack (photosyns_inst = _out[7]) is dropped since its components bound at the call.23. A nested function's
returnis its own. PhotosynthesisHydraulicStress carries itsft/fth/fth25statement functions as nested defs; their returns counted as early returns of the kernel and the single-exit rewrite refused the routine ("an early return in a function with several outputs").24. A nested def at the top of a body is not descended by the early-return scan (the statement functions again, as the first statements of the body: the scan entered them from the root).
25. A loop around nothing lowers to nothing.
brent_PHSchecks its bracket per phase and ends the run; with the raise dropped the loop carried nothing and the backend refused the kernel ("loop with no carried effects"); as a guard around nothing already did, it lowers to nothing.26. A module's own state object is spelled by its components in a kernel. The anchor spells PhotosynthesisMod's own
params_instbare (params_inst.psi50insideplc); the flat spelling knew dummies and companion state only, so every hydraulic-stress helper kernel tookparams_instas an opaque parameter no calling kernel had -- latent while the SP recordings never ran them,NameErrorthe first time the hydraulic-stress kernel did.27. A kernel's state closure is what its body still names. With the object spelled flat, the record's
module_state_readstill saidparams_inst, and the closure taken from it gaveplc's kernel an opaque trailing parameter that every calling kernel passed bare.28. A while loop's done flag starts False at the top of the kernel.
calcstressruns its Newton loop insideif (.not. night); the branch carried the loop's flag and thelax.condoperand tuple read_done_1before anything had bound it (UnboundLocalError). Like the goto-region flags, every done flag is initialized at the top.29. Consecutive guarded statements share one block. After an early return or a break each following statement was wrapped in a guard of its own, so a call and the unpack of its result became two
lax.conds and the temporary the first bound was not in the second's carry (NameError: _t1inbrent_PHS); a statement that returns or breaks closes the block.30. A Fortran-order copy is the array itself in a kernel.
transpose(a)isnp.asfortranarray(a.T)in the anchor, a layout the F-order reshape downstream relies on;jnphas no layouts and noasfortranarray(SimpleMathMod's kernel raisedAttributeErrorunder the hydraulic-stress kernel).31. A name that sizes an array or bounds a range is a Python int in a kernel.
brent_PHS'sinteger, parameter :: nphs = 2sizes its bracket arrays; the backend strengthened the literal tojnp.int32(2)for a stable carry dtype andjnp.zeros((nphs,))refused a traced shape.32. An alias bound twice to one target is still an alias. The anchor binds a pointer alias twice, as the view it opens the body with and again where the Fortran's
=>stands; counted as two stores it was a plain local, rebound in the loops and never written back, and the hydraulic-stress kernel returned every output as it came in (the gate read 9e18 ULP). Every top-level store binding the same value is one alias.33. Nested capped loops have flags of their own. The capped
whilelowering gave its counter back, sohybrid_PHS's two capped loops were both_done_1and the inner loop's exit ended the outer after one pass (the kernel'siter1stayed 1 where the anchor ran to 4).What the nutrient-limited path forced (
Photosynthesisunderuse_cn = .true.,nu_com = 'ECA', recorded on a third sibling case held in accelerated decomposition; bit-exact on 129,024 points in NumPy)34. A return inside a loop ends the pass and the loop: the statements after the flag in any block run under
if not _ret, awhiletakes the flag in its test (its body stays a body, so the cap it breaks on stays in the capped lowering's sight and the loop remains a fixed count of passes), afor, which has no test, runs its whole body under the flag -- nobreak, since aforlowers to a scan whose body cannot break.brentreturns from inside its bracketing loop ahead of the nextci_funcevaluation; the flag alone left the rest of the pass and the following passes running, and the kernel'sgs_molwas an evaluation the Fortran never made (this recording is the first on whichhybridreachesbrent).35. A store inside an inner loop, and a subscript store, is a store of the enclosing block: the carry of a
while_loopis computed before lowering, andbrent_PHS'sc(phase) = ...inside itsdo phaseloops was counted by neither walk (UnboundLocalError: cin the pass).Full suite passes (
tests, minus the C kernels and JAX-diff suites).🤖 Generated with Claude Code
https://claude.ai/code/session_01BwmCpkkFKaGihSPvqAQnaK