Flattening CLUBB's grid: allocations by any name, bounds by component, generics by arity - #27
Open
chenyueqi wants to merge 100 commits into
Open
Flattening CLUBB's grid: allocations by any name, bounds by component, generics by arity#27chenyueqi wants to merge 100 commits into
chenyueqi wants to merge 100 commits into
Conversation
CLUBB's grid: setup_grid allocates every component in one statement, allocate( gr%zm(ngrdcol,gr%nzm), gr%zt(ngrdcol,gr%nzt), ... ), naming the object by its dummy rather than this, sizing an axis by another component of the same object and another by the module's private parameters. The allocation reader takes any object name and every component of a statement; a bound spelling obj%comp is bound to that component's flat name, and the component is carried as an input whether or not the body reads it; a bound from one is spelled by its upper bound alone; and the driver's extent is declared once when the subprogram already takes it as a dummy, as CLUBB's do with ngrdcol. The read/write scope also learns a sibling's generics (companion_externals): a call spells zt2zm_api, and a name the scope did not know as a procedure it counted as a read of data. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ad; a stub under control flow is a stub block calc_pressure declares thvm(ngrdcol, gr%nzt): a dummy sized by a component of the object it takes. The flat signature spells that component's flat name, an argument of the adapter, and the plan carries the component as an input whether or not the body reads it -- also when another object's allocation names it (sponge_layer_damping's profile, sized by gr%nzm). Every dummy's type is read before the objects are walked so the reference resolves whichever order they come in. Skx_func(..., Skw_zm) into a sibling: the caller's storage is passed in and returned, so the caller reads the actual as well as writing it. The companion table now says which positions are buffers, and the read/write scope reads them, the rule it already applied to its own subprograms. if ( stats%l_sample ) then / call stats_update(...) / end if is a framework stub under a condition. The condition is read on both sides and the only disagreement is the stub's actuals; the block is waived like a bare stub, and the verdict names the calls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ate state left to its module, ungated means not sampled An object passed to a sibling through a generic (zt2zm_api over grid_class's specifics) was not followed, so the adapter built a record missing the components the specific reads. A generic is followed into every specific; the union is what the adapter carries. An allocation sized by the allocating routine's dummy (coef_wp4_implicit(1:ngrdcol,1:nz)) is spelled by the planned subprogram's dummy of the same name, an argument of the adapter. A module variable the module keeps private (error_code's clubb_debug_level) reaches no use statement, so no adapter sets it; both sides run with the module's default and the plan records it under left_to_module instead of failing. A subprogram the operator declared ungated is not sampled by the bit-exact gate either: the declaration says the reference cannot be held on generated inputs, and rcm_sat_adj proves it by error-stopping the run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…rsion guard is a stack Following every specific with the caller's positional actuals mapped the object onto whichever dummy sat at that position -- zm_min for the one-level specific -- and the specific the call actually reaches was then skipped, because a nested follow had marked it visited first. The specifics whose argument count fits the call are followed (the exact count when any matches), and a key is released when its follow returns: the set guards against cycles, it is not a memo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ct is read off what the translation returned; a translated error stop is a failed comparison CLUBB's constants_clubb declares ep = Rd / Rv over two real parameters. The fold's rule -- an initializer with no real literal is integer arithmetic throughout -- made it zero, and every unit reading ep1 or ep2 computed NaN. The resolver now records each constant's declared base type, the fold types a name by it, and both renderers carry the environment along in dependency order; nrk = runge_kutta_type / 10 stays 4. The Python flat adapter read a written component off the record it built from the inputs even when the dummy was intent(out) and the translation returned a new object (sponge_layer_damping's profile): it now rebinds to the returned object first. A translated bare error stop raises SystemExit, which the bit-exact gate let through and which ended the run with no report; it is now a comparison that could not be made, named as such. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
CLUBB's lscale_width_vert_avg searches with do k_avg_upper = k, ...; if (...) exit; end do and integrates up to k_avg_upper. On completion Fortran leaves the index one step past the end, m1 + n * m3; Python's for leaves the last value, and the integral ran one level short -- a NaN on one side in wp23_term_splat_lhs. For a loop whose index some later statement of the subprogram names, the emitter adds a for-else that sets the completion value; an EXIT is a break and skips it, as Fortran keeps the exit value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
gfortran's inlined SUM is a loop in element order; np.sum pairs its terms and rounds differently -- CLUBB's vertical_integral drifted 12 ULP on 3 of 7770 points. _f_vsum, beside _f_vdot, walks the array in Fortran order or along the named axis, and the intrinsic table points SUM at it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ned; the corpus re-baselined An else on every loop whose variable any later statement names put one on most loops of a module -- i and k are reused by the next loop -- and moved the translator differential from 33 to 130 differing subprograms. A later DO over the same variable or an assignment to it redefines it first and closes the question; a later loop's bounds may still read it and count. The differential is at 45, every one of the twelve new differences an intended class: an array-valued .or. spelled |, SUM through _f_vsum, the completion value on a search loop. The corpus, re-run on this branch: no unit regresses; csplines, fitpack and slsqp_core have fewer read/write disagreements; numfor's sorting reaches the bit-exact gate for the first time (its specifics are private behind a public generic) and exposes a bare use-imported constant in a local parameter, issue #28. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…s the run owns A character component (err_info%err_header) has no flat spelling and the physics reads it only to print: left at the object's default, under left_to_module. A derived-type module variable declared in a list over several lines (the four sponge settings) is found on the module record when the one-line pattern misses it. An allocation sized by the allocating routine's dummy when the planned subprogram has none of that name (coef_wp4_implicit(1:ngrdcol,1:nz) under advance_wp2_wp3, which takes nzm and nzt) becomes an integer argument of the adapter, extent_args, which the recorder writes from size() and the sampled gate sizes like any scalar an array's dims name. Every advance_* plan of CLUBB is usable on this. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…es it CLUBB passes ngrdcol as a dummy of every subroutine the recorder probes; a local of the same name was a second declaration, and gfortran said so forty times. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
CLUBB continues its calls as a, b, & ! In on every line: the comment after the ampersand left a blank the joiner did not strip, and the probe carried & into its argument list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
CLUBB allocates its scalar-tracer coefficients only when sclr_dim > 0; under bomex it is 0, and reshape of an unallocated component faulted the probed run after three dumps. An allocatable or pointer component is recorded inside allocated()/associated(), as a zero-extent record otherwise, and the extent argument taken from its size() is zero when it was never allocated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…eads and writes nothing The read/write scope marked whatever sat at a union of positions as written when a call reached a sibling's generic whose specifics differ in arity: CLUBB's tridiag_solve and zm2zt_api marked nzt, l_implemented and gr. The companion table now carries each specific with its argument count, and the scope picks the one the actuals fit. A name use-imported from a stubbed module is a call the translation answers with a stub -- pass, for stats_update -- so the source side no longer counts its actuals: stats_tmp and the budget names were reads only that side saw. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
The first cut put every name imported from a stubbed module into the externals table, and a bare name in that table is not a read on the source side: every constant of constants_clubb -- a stub and a table of constants -- became a read only the target saw. The frontend now asks the stub module's own record which imports are procedures (an absent stub is taken at its import list), and hands the list to the translation, whose protocol lists them as procedures so a call to a stand-in function is a call and not a read of its name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…left off The companion table said which positions a call writes and left the reading to 'everything else', so an INOUT actual -- lhs, rhs, err_info into CLUBB's band_solve -- was written and not read on the source side. Each entry now says what the caller reads: IN, INOUT, UNKNOWN and a buffer OUT. And a specific of a generic fits a call whose actuals fall between its required and its total count, since an optional dummy may be left off. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
call band_solve( ..., solut, rcond = rcond ) (CLUBB): bound by position the keyword fell on the dummy before it and was counted a read; the companion table now carries the dummies' names and the scope binds a keyword actual by name, as it does for a callee of the module itself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ank-picked specifics, stubbed calls raise in place Found by replaying CLUBB's advance_* solvers on bomex recordings: * dump-replay's parser took only numbers: a logical ``T``/``F`` header (l_implemented) was no value, and a zero-extent component (scalar tracers under sclr_dim = 0) vanished with its empty body. Both are values now. * The recorder wrote what the objects received; the plain OUT/INOUT dummies (wp2, wp3, mono_flux_limiter's low_lev_effect) are outputs too. * Two specifics of one arity (tridiag_solve with its optional rcond and the multiple-rhs one without) were picked by count; the ranks of the bare-name actuals decide, so an IN logical is no longer scored written. * A component name on an OUT actual (``pdf_params%chi_1``) was counted a read of a variable of that name; the tidier answer the pipeline's test kept as a documented disagreement is now the answer on both sides. * A call to a procedure of a stubbed module that no statement stub answers (lapack_band_solvex on CLUBB's LAPACK path) deferred its whole block, condition and LU branch included, so the candidate raised on the path the run takes. It raises on its own line now. * ``max(2, edsclr_dim)`` sizing a local is a bound Python can spell; a comma in a bound is legal inside max/min alone. * The flat-plan module lookup tolerated a missing record (mypy). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
``call inner( x, y, rc = rc )`` where ``rc`` is the caller's own optional OUT was rendered ``want_rc=True``: present in the callee on every call. CLUBB's xm_wpxp_solve hands ``rcond = rcond`` to band_solve, whose ``present(rcond)`` branch is the LAPACK diagnostic path -- taken, on the replay, on every call the run made without rcond. The presence is the caller's own sentinel for an optional OUT and ``is not None`` for an optional IN. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
The source side of the read/write gate scored call solve( ..., rc = rc ) as a write of the caller's own optional rc; the translation spells the callee's present(rc) as the caller's want_rc sentinel, a read. Both sides now count the read, for a sibling's procedure (companion externals carry the optional OUT positions) and for one of this module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…sion over a constructor is a value (#26) * The flat plan followed only the caller's own dummies into callees. A module-state object passed whole to a derived-type dummy -- CLUBB's advance_xm_wpxp hands sponge_layer_damping's profile to sponge_damp_xm, which reads ``profile%tau_sponge_damp`` -- was left to the module, and the replay found it unallocated. It is followed under the callee's dummy now, so its components come back under the state's own name. A derived-type state variable is no longer listed as left to the module: it is the objects path's business. * ``100._core_rknd * (/ ... /)`` as a local parameter (#26, saturation and pdf_closure's hybrid-PDF path): the token pass rendered a bare constructor and handed anything around one to the parser, whose literals were never hoisted. Each constructor is rendered where it stands and the rest goes through the token pass, elementwise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
CLUBB's finalize_tau_sponge_damp_api deallocates a component and returns; its adapter returns None, which the gate counted as one value against zero out-intent arguments. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
The declaration carries the reason -- CLUBB's sponge initializer leaves the levels below the layer undefined on both sides -- and the replay reported it, then compared heap contents against np.empty anyway. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
No unit regresses; numfor's genrand goes from 7 to 3 disagreeing blocks and slsqp from 12 to 8 (the component-name and handed-on-optional rules). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
The comparison loop takes main's: a translated ERROR STOP or an IndexError on a draw is a draw the subprogram does not take, redrawn. This branch's recorded-sample branch, zero-value return and declared-ungated skip sit beside it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
Against main's own baseline no unit regresses; slsqp goes from 15 to 7 disagreeing blocks of the same 106 checked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
cpp leaves blank lines where an #ifdef stood inside CLUBB's advance_clubb_core call; the probe took one for the end of the statement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
CLUBB's advance_clubb_core passes thlm2T_in_K_api( ... ) -- a generic over a scalar and a 2-D specific -- as an actual to sat_mixrat_liq_api. Ranked scalar without looking, the outer generic matched no specific, its block was deferred, and a deferred block takes the whole subprogram out of the gate. A non-elemental function's rank is its declared result's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…eftover CYCLE is delegated CLUBB's interpolators cycle from inside an IF in a DO loop. The lowering passed the continue through into a lax.cond branch -- a SyntaxError that took the whole emitted module down. The loop body's continuation now moves into the branches that do not cycle, before the fori_loop lowering; what that cannot fold, and every break, delegates the subprogram to the host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
iipdf_type == _mod.IIPDF_ADG1: pdf_closure picks its PDF by a constant of model_flags spelled through the module alias. The static-test predicate (and the tree's static-expression one) took only the bare upper-case spelling; the dispatch was a lax.cond chain, every arm traced, and the arm the run never takes carried a host driver's record object into the cond. Static now, the dead arm is never traced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…s fallback Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
The expression mapping spells .not./.and./.or. as jnp.logical_*; under jit those stage a constant into a tracer that no Python if can convert (interpolation's .not. l_quintic_poly_interp took the whole step down). The Python form of a static branch is now spelled with Python's not/and/or, _f_concrete decides on the leaves (the comparisons, the names), and the static predicate sees through the mapped calls so a negated static comparison is static too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
… are its lowered stores A DO WHILE under an IF inside a DO (interpolation's grid search): the exit flag is a carry of the enclosing cond and needs a value before the branch, at the top of the function like the goto-region flags, with the one beside the loop resetting it on entry. And the loop's carries are what its lowered body stores: a subscript store is a store to its base only once lowered, and an inner loop's break flag and kept index are the body's own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ried by enclosing loops advance_helper's window search: a DO with an EXIT under an IF inside the column loop. The flag and kept index initialized beside their loop were carries of the enclosing cond with no value before the branch; they now start at the top of the function (False, index 0) and every loop enclosing a store of them carries them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
A DO variable's completion value, max(0, (ub - lb) // step), traces when the bounds do (an inner loop over the outer index): Python's max on a tracer is a boolean conversion. _f_pymax/_f_pymin keep Python scalars Python (a static bound stays static) and fold tracers with jnp.maximum/minimum. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
jax.numpy has no erf; the math-to-jnp mapping spelled jnp.erf for the anchor's scalar math.erf (CLUBB's cloud fraction). Both spellings now reach _f_verf/_f_verfc, which import jax.scipy.special. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
advance_xm_wpxp counts its right-hand sides under switches and a local derived from them, then sizes its solver arrays by the count. A local whose stores are all trace-time expressions -- at the top level or under branches decided at trace time, logical forms included -- is concrete: the tree tells the backend on a marker it strips, its branches are Python ifs, its stores are never strengthened to jnp scalars and are spelled with Python logic, and zeros((n, nrhs)) sees a Python int. Logical scalar dummies are static in the tree as in the backend. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…turn value is shaped like the final one A trace-time expression stored after a traced early return sits under the return's guard and is a carried value there (mixing_length's start_index beside a NumPy int32 static): judged on the body after _single_exit, it is strengthened like any carry, and a name any pass finds stored under a loop or a traced branch stays not-concrete for the passes that follow. The single-value early return's placeholder is zeros_like the final value, placed after the guard inits, so an array output beside a scalar placeholder is no longer a cond with unequal arms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…kes the local's guard type advance_xm_wpxp picks statistics names by the solve type under a traced branch: no JAX type carries a string, and with statistics off nothing reads it but the dropped calls, so it stays a trace-time value outside every cond and loop carry. And a store of an expression over static arguments into a guard-typed local is cast like a constant's: statics arrive as Python ints through the jit wrapper and as NumPy int32 from the gate, and a cond arm must not follow the spelling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…nd for the single-value exit A trace-time value stored after an early-return check is one still: the guard is not a branch on data. The concreteness scan sees through it; a concrete local's store is int()/float() of its expression so a cond that carries it sees one Python kind on both arms whatever the static arguments' spelling; and the single-value early-return rewrite wraps the whole remainder in one guard, as the tuple path does, so a value set after the check reaches the rest as a Python value and not as a cond carry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ss scan advance_xm_wpxp counts its right-hand sides under clubb_at_least_debug_level_api(0): a framework stand-in's function of constants, a Python value at trace time that the backend's static test already takes. The concreteness scan now takes it too, so the count under it stays a trace-time value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…ic test A framework stand-in's alias is not in the tree's module table (the flat rewrite leaves such calls alone), so the rule keyed on it never fired for clubb_at_least_debug_level_api(0). Any module function of trace-time values is one, the backend's own rule; a ported kernel's call is still scalar_port_call's business. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
… concreteness scan advance_xm_wpxp counts its right-hand sides under iipdf_type == _mod.IIPDF_NEW: the static-expression and static-test predicates took the alias spelling already; the concreteness scan did not, and the count under it was strengthened into a tracer that sized the solver arrays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
A kernel's implementation is also called from another kernel's traced body (the dual form: mixing_length's plume loop reaching the saturation kernels), where the static arguments are tracers and Python's float()/int() is a concretization error. _f_pyint/_f_pyfloat give one Python kind for a NumPy or Python scalar and leave a tracer a tracer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…hor's wrapper A branch that only picks statistics names (string stores, never carried) has nothing a cond could carry and is dropped like a guard around dropped logs, instead of delegating the routine. And a same-module call the backend keeps on the host is kept only for a name the anchor module defines: a private flat function has no wrapper there, and a line binding it broke the import (the whole step's advance_xm_wpxp); its caller is delegated with it, as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
The solve-type dispatch that picks statistics names nests a static if inside the traced one; triviality now recurses through it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
Keeping a same-module call on the host only for a name the anchor defines re-delegated pdf_closure_driver_flat (its dead call to the zm variant has no wrapper) and with it the whole step. A missing attribute inside a branch the run never takes is never evaluated; a live one fails by name, which the gate reports either way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
fill_holes stores start_indx = int(lower_hf_level + dir * n) under a traced branch: a Python int at trace time beside the int32 guard init, a cond with unequal arms. The static-expression predicate sees through the anchor's int()/float()/np.* conversions, so the store takes the local's guard type like any other. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
The anchor spells a DO variable read after the loop as a for-else even when nothing exits the loop (advance_xp2_xpyp's solver loops): the completion value always runs, cast to the index's dtype, after the fori. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…spell pdf_closure_driver writes the scalar index into a statistics name (an internal WRITE, then adjustl): not a literal store, so the literal-only rule carried it into a cond. A local whose guard init is a string is a character local: never a carry of a cond, a fori or a while, and a branch that only stores to one is trivial. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…hims A kernel body still reaches _f_list_write / _f_fmt_write (statistics names written from an index), _f_cfold (a compile-time-folded constant, mpmath lazily as in the NumPy runtime; the contract test's JAX exemption takes it) and _f_rstep / _f_rstep_lb (Python slices from static bounds). A tracer handed to a writer prints as a placeholder: with statistics off nothing reads the name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
pdf_closure_driver's scalar loop writes the index into a statistics name and nothing else: with character locals never carried, the loop carried nothing and the driver was delegated. Trivial bodies (what a cond drops) drop the loop too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…s the anchor's destination The whole step's first divergence: the mixing length calls saturation's sat_mixrat_liq_2d with its optional start index, the plan's adapter leaves every optional out (the recorder calls without them), and the flat kernel silently computed from level 1. A flat kernel now takes its optional IN dummies as parameters defaulting to None, a call passes the ones the caller passed, and present() sees them. And an OUT array the anchor does not pass (it takes the result back) fills the flat signature's caller-buffer slot with the anchor's own destination. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
A flat callee's call elides the anchor's _out buffer (its outputs bind at the call); a plain companion kernel's _out = clip_covar(...) that follows is real, and so are its _f_copy_out unpacks. The stale elision dropped them (xm_wpxp_clipping_and_stats kept the unclipped fluxes): a rebinding by anything but a flat callee ends it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
…rns them The whole step's clipping counters never moved: advance_xm_wpxp hands xm_wpxp_clipping_and_stats its counter by keyword (``wpxp_cl_num = wprtp_cl_num``, an optional INOUT the callee increments), the flat kernel took only optional IN dummies as parameters, so the counter started absent, the increment never traced and the caller's count stayed put -- silently (NumPy and Fortran agree, JAX lagged by one per call). A flat kernel now takes an optional INOUT dummy as a parameter defaulting to None and an optional OUT dummy as its ``want_<name>`` sentinel (the anchor's convention, static under jit), returns both after the plan's outputs (None when absent), a kernel calling it passes what its caller passed and binds what comes back (a buffered anchor's ``rcond = _out[k]`` through a temporary), and the wrapper the adapter calls drops them (``optional_returns`` on the flat interface record). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENnuj29L6PbYh8BLEocNqD
A dropped optional output binds to ``_``; carried, a branch that did not assign it read it before assignment (advance_xm_wpxp's stats-only xm_wpxp_solve call). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENnuj29L6PbYh8BLEocNqD
…blocks, 56 deferred) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENnuj29L6PbYh8BLEocNqD
CLUBB's pdf_closure_driver_zm (live under e3sm_maint32's
l_call_pdf_closure_twice) builds a local implicit_coefs_terms, hands it
to init_pdf_implicit_coefs_terms_api and pdf_closure, and drops it; the
flat rewrite knew only dummy and module-state objects, refused the call
("object not passed"), and the whole step fell to a host wrapper that
does not exist. The anchor's ``tmp = _make_T()`` now declares a local
object whose components are the locals ``tmp__<comp>`` (None until a
callee creates them); an OUT object the anchor takes back whole binds
through the assignment's target; an unallocated component handed to the
callee that allocates it goes through ``_f_buffer`` as zeros of the
callee's declared extents spelled with the call's actuals, because the
callee reads its buffer's shape.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnuj29L6PbYh8BLEocNqD
…inant element A backend whose transcendentals are not libm's is an ULP off at some arguments, and a model can amplify that into a dominant element past the ULP gate (CLUBB's cloud fraction, erf of a saturation deficit, under a trapezoidal average: 128 ULP of wp2thvp from 1 ULP of erf). The gate failed that as a translation defect. Now, when the dominant tier fails with the relative tier inside its gate on a recording, the candidate's own bit-exact anchor is rerun on the worst recorded samples with every transcendental's result moved one ULP (up, down, by a seeded coin -- recast.verify.conditioning), and compared to the recording by the same dominance rule. A candidate whose dominant ULP distance is inside that spread is TOLERANCED with the evidence on the verdict (metrics["conditioning"]); one past it stays FAILED, with the same evidence. ``conditioning: off`` disables the check; the comparison now keeps per-sample distances on a recorded outcome for it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ENnuj29L6PbYh8BLEocNqD
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.
Stacked on #25. What the flat plan, the gates and the emitter needed to take
type(grid) :: grand its neighbours through, found on the CLUBB extension's tier-1 units.Flattening
allocate( gr%zm(ngrdcol,gr%nzm), gr%zt(...), … )— one statement, many components, the object named by the setup routine's dummy rather thanthis.gr%nzm) is spelled by that component's flat name and the component is carried as an input; a dummy declaredthvm(ngrdcol, gr%nzt)the same; an allocation sized by the allocating routine's dummy (1:nz) by the planned subprogram's dummy of that name; a bound from one is its upper bound alone.ngrdcolis declared once when the subprogram already takes it.error_code % clubb_debug_level) is left to the module, underleft_to_module.intent(out)object's written components are read off the object the translation returned.Read/write scope: a sibling's generics are procedures to it; a sibling's buffer-OUT actuals are read; a stub under control flow (
if (stats%l_sample) then / call stats_update / end if) is a stub block.Two silent translation defects, both fixed with executed tests
forleaves the last value.lscale_width_vert_avgintegrated one level short. A loop whose index a later statement reads before anything redefines it gets afor … elsewith the completion value; an EXIT (break) skips it, as Fortran keeps the exit value.ep = Rd / Rvover two real parameters was folded with integer division (the old "no real literal means integer arithmetic" rule) and came out zero; every unit readingep1/ep2computed NaN. The resolver records each constant's declared base type and the fold types names by it.Bit parity: SUM accumulates in element order (
_f_vsumbeside_f_vdot); np.sum's pairwise rounding drifted 12 ULP invertical_integral.Gate: a subprogram the operator declared ungated is not sampled; a translated bare
error stop(SystemExit) is a failed comparison, not the end of the run.Tier-2 replay (recorded runs through
dump-replay), found on CLUBB's advance_* solvers replayed on bomex recordings:T/Fheader (l_implemented) was no value and a zero-extent component (scalar tracers undersclr_dim = 0) vanished with its empty body. Both are values.wp2,wp3, mono_flux_limiter'slow_lev_effect) are outputs too.tridiag_solvewith its optionalrcondand the multiple-rhs one without) were picked by count; the ranks of the bare-name actuals decide.pdf_params%chi_1) was counted a read of a variable of that name — the documented disagreement inherited from the pipeline, resolved the tidier way now that a translation showed it (test flipped, deliberately).lapack_band_solvex) deferred its whole block,if ( method == lapack )included, so the candidate raised on the LU path the run takes. It raises on its own line.max(2, edsclr_dim)sizing a local is a bound Python can spell; a comma in a bound is legal insidemax/minalone.rcond = rcond) carries the caller's presence (want_rcond=want_rcond, notTrue) — xm_wpxp_solve took the LAPACK diagnostic path on every call — and the read/write gate counts that hand-on as a read of the optional on both sides.sponge_damp_xm) is followed under the callee's dummy, so the plan carries the components the callee reads; a derived-type state variable is no longer listed as "left to the module".100._core_rknd * (/ ... /)as a local parameter — each constructor rendered where it stands, the rest through the token pass, elementwise (saturation; pdf_closure's hybrid-PDF path).finalize_tau_sponge_damp_api); a declared-ungated subprogram is not compared on a recording either, and the reason is reported.Merged with main (2026-09-04, after PRs #29 and #30 landed there): main's comparison loop is kept as is — a translated ERROR STOP or an IndexError is a draw redrawn — with this branch's recorded-sample path, zero-value return and declared-ungated skip beside it; constant folding carries main's parameter aliases together with this branch's intrinsic calls; the appended tests of both sides are kept. Merged bottom-up through #22 and #25 so each PR stays mergeable.
Held to (self-check 2026-09-04 before tier 3): full suite and mypy green;
conformance/ --plugin-set recast-clubbgreen; the corpus re-run on this branch regresses no unit (numfor's genrand 7→3 disagreeing blocks, slsqp 12→8, csplines/fitpack/slsqp_core as before);tools/emit_diff.pyagainst the translator goes from 34 differing subprograms at this branch's base to 48, every added row an intended class —_f_vsumfor SUM,want_x=want_xwhere an optional OUT is handed on (wvsat'sqsat*: the translator'sTrueis the defect fixed here), declared-initializer locals, anddefine_swept_areaswhere the translator drops thereshapearound a constructor and this branch keeps it (#26). CLUBB tier 0 stays 12/14; tier 1 is 7/11 bit-exact withgrflattened (advance_helper_module: 25 subprograms, 7,770 points); tier 2 (summaries/tier2.jsonin clubb-jax): 10 of 12 units bit-exact on 20–40 recorded calls each from bomex and cgils_s6 under the hybrid PDF — advance_wp2_wp3, advance_xm_wpxp, advance_xp2_xpyp, advance_xp3, advance_windm_edsclrm, clip_explicit, mono_flux_limiter, mixing_length (incl.diagnose_lscale_from_tau), pdf_closure, new_hybrid_pdf_main — plus the sampled plain routines of clip_explicit, mixing_length and pdf_closure. sponge_layer_damping compares nothing (initializer's below-layer levels are undefined on both sides, declared; finalizer has no output; thesponge_damp_*functions takegrand have no flat plan); new_pdf_main has no recording (the new PDF needs LES input fields).Tier 3 (2026-09-04):
advance_clubb_core— the whole CLUBB time step, 135 dummies flattened to 476 arguments over 7 derived dummies and the 14 sponge state objects — is bit-exact on 40 recorded steps (20 bomex, 20 cgils_s6 under the hybrid PDF): 453,700 compared values, no rewrites to notarize. Two more fixes got it there: a probe spans blank and comment lines inside a continued call (cpp leaves them where an#ifdefstood), and a generic function reference is ranked by the specific it dispatches to (a nestedsat_mixrat_liq_api( ..., thlm2T_in_K_api( ... ) )deferred a block, and a deferred block took the subprogram out of the gate).🤖 Generated with Claude Code
https://claude.ai/code/session_01Cne4hrGgYqhTMzVgzJtXYd
2026-09-05 (late): optional INOUT/OUT dummies; whole step in JAX
want_<name>sentinel) as parameters and returns them after the plan's outputs; the adapter's wrapper drops them (optional_returnson the flat record); the discard name is never a carry. Found on the whole step (advance_xm_wpxp's clipping counter handed to xm_wpxp_clipping_and_stats by keyword never moved); the recorded advance_xm_wpxp JAX port had regressed to 160/200 integer mismatches since c4e8c82 and is bit-exact again.--xla_disable_hlo_passes=algsimp, set by the case runner beside optimization level 0). algsimp's rewrites of double arithmetic leave a jitted kernel 1-2 ULP from its eager evaluation, and the step turns 2 ULP in the mixing length's tau into a different vpwp through xm_wpxp's near-singular penta solve (measured by nudging tau in the exact eager step). With the flag the ten recorded units are 8 bit-exact, 2 within the gate; the whole step has 453,524 points bit-exact, max 53 ULP, max_rel 3.2e-15.2026-09-05 (night): a local derived object is its components
tmp = _make_T()declares a local object whose components are the localstmp__<comp>(None until a callee creates them); an OUT object the anchor takes back whole binds through the assignment's target; an unallocated component handed to the callee that allocates it goes through_f_bufferas zeros of the callee's declared extents in the caller's terms. Found on CLUBB's e3sm_maint32 configuration (l_call_pdf_closure_twicemakes pdf_closure_driver_zm live, which builds its own implicit_coefs_terms); with it the whole step lowers fully under that configuration too.2026-09-06: the tolerance gate measures conditioning
recast.verify.conditioning) and compared to the recording by the same dominance rule. Inside that spread: TOLERANCED with the evidence on the verdict (metrics["conditioning"]); past it: FAILED with the same evidence;conditioning: offdisables it. On CLUBB's e3sm_maint32 configuration the whole step's 128 ULP sits against a 17,320,256 ULP spread of the anchor.