Repository navigation
fix(eq): eq_save refuses after a set of tot's eq: parameters too - #267
HengyuLi-Ozaki-lab wants to merge 1 commit into
Conversation
Follow-up of the reviews of k-yoshimi#256 and k-yoshimi#257, and of the maintainer's comment on k-yoshimi#256 of 2026-10-17 (the k-yoshimi#253 counter for eq). eq_save's precondition was a flag that eq_api_set_param cleared. tot's "eq:" dispatcher (tot/tot_param_registry.f90) calls eq_param_registry directly, so after tot_set_param("eq:RR", ...) the flag stood and eq_save wrote the parameters as they stood over fields computed for the old ones. As k-yoshimi#259 did for tr, fp and ti: - eq_param_registry counts every successful numeric set (eq_param_generation()); eq_param_set is now a wrapper around the former body (eq_param_set_impl). Not counted: MODEFR and MODEFW (they say how a file is opened; the base name is taken as the setter reads it), and the string sets (eq_param_set_str, KNAMEQ and the other KNAM*), as eq_api_set_param_str already left the flag alone: the solve reads none of them, a load records the count itself, and KNAMEQ is where eq_save writes. A refused set is not counted. - eq_api records the count when eq_run(0) or eq_run(1) succeeds, and eq_save refuses (EQ_ERR_INVALID, nothing touched) when the flag is down or the count moved. The clear in eq_api_set_param is gone. - eq_api.h and the eqlib README: tot's eq: parameters are now seen; what no registry sees in the mono image (another module's eq_init, eq_load or eq_calc, a plcomm variable set through another module's registry such as ti:RR) is k-yoshimi#260. The smaller points of the two approvals: - eq_mcp's save tool: the MODEFW it sets stays set afterwards. - eq/eqfile.f90, comment only: the length comparison depends on the run-time; one that reports the length on disk, or -1, makes it a no-op, not a false failure. - test_save.py: a refused write leaves at most the limit (the run-time's buffer decides how much was flushed). - _encode_name's docstring: KNAMEQ takes 80 bytes, not 79. - python/_fd_isolation.py: fd 2 is repaired only when fstat says EBADF; another error means fd 2 is there, it is left alone and the installation fails before changing anything. A host's sys.stdout whose flush raises an Exception (not only OSError / ValueError) no longer stops the installation. Not taken: an os.isatty(1) exemption for a server started by hand in a terminal. A host may run the server on a pseudo-terminal and parse that stream; the READMEs (k-yoshimi#264) say how to keep the library's output (2>server.log). Tests: test_save.py, a refused set and a string set leave the save allowed; test_set_after_run.py, on the mono image (CI's mono routing step already runs that file; the cases sit at the end of the file), eq_save writes after tot_init, MODEFW=0, eq_run(0) and tot_set_param_str("eq:KNAMEQ"), refuses after tot_set_param("eq:RR"), writes after tot_set_param("eq:MODEFW"); test_fd_isolation.py, an fd 2 that fstat cannot look at is not replaced, a failing flush does not stop the installation. The fd isolation's two points get a CHANGELOG entry of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k-yoshimi
left a comment
There was a problem hiding this comment.
Review through the full cycle (Codex + in-house domain, test-oracle lenses + docs/workflow review), with eq/libeqapi.so and libtotapi_mono.so rebuilt from this head (the mono image with the CI recipe) and the tests run under the CI flags.
Verdict: approve. The counter holds on every path the reviewers traced: eq_set_param and tot's eq: numeric dispatcher both go through the new eq_param_set wrapper (tot/tot_param_registry.f90:145-150), the string paths reach eq_param_set_str and are uncounted by design, eq_run(0)/(1) record the count only after success and clear the flag before the attempt, eq_init/eq_finalize clear the flag so the never-reset g_equilibrium_generation cannot admit a save after finalize → init without a run, and tot_finalize reaches eq_api_finalize. EQSAVE's record (eq/eqfile.f90:65-100) consists only of counted registry entries; EQCALC recomputes from scratch each call, so eq needs no re-prep. The MODEFR/MODEFW exclusion uses the same parse_array_subscript + TRIM compare as the setter itself, so MODEFW[1] is treated the same on both sides.
Runs: eqlib + test_fd_isolation.py + eq_mcp: 161 passed, 3 skipped. Mono image from this branch: the three new TestEqSaveAfterTotSetOnMonoImage cases pass. The test-oracle reviewer also ran that class against develop's mono image (no eq_param_generation symbol): only test_eq_save_refuses_after_a_tot_set fails there, (0, True) != (1, False), so the test discriminates and the control cases do their job. CI green at d7f60d55; the mono step runs this file with MONO_LIB_PATH.
LOW, fold in if you like (none blocks):
python/eqlib/eqlib.py:317-326— theEq.savedocstring still says "set_param of anything but MODEFR and MODEFW"; the README andeq_api.hgot the toteq:/ string-set / #260 sentences, the API docstring did not. Same foreq_mcp/server.py'ssavedocstring ("the two file modes") and the eq_mcp READMEsaverow /MODEFWdescription (MODEFW stays set afterwards).test_save.py:338<= limitadmits a 0-byte file; the raise and theXX EQSAVEline still pin the failure path, but0 < size <= limitkeeps the docstring's partial-write claim without depending on the runtime's buffering.test_fd_isolation.pyEIO case: the old code also raises EIO (from_same_fileafter it had already replaced fd 2), sosame_fd2=Trueis the discriminating assertion; a separate assert with its own message would keep that visible._EqSaveCase"writes" cases:os.path.getsize("eq.bin") > 0instead ofexistspins the write independently ofeq_api_save's own non-empty check.
Noted, no change asked: NPRINT and IDEBUG move the counter although they only steer printing and do not change the solve's result — same as the pre-diff flag through eq_set_param and as tr/fp/ti count, and the documented exclusion set is exactly the one implemented. Pre-existing and outside this diff: eq/eqfile.f90:184 EQ_READ's MODELG=15 branch calls EQDSK without IERR, so eq_run(1) could record a generation over a load that failed; and tr's run path rewrites eq's NRMAX/NTHMAX/NSUMAX through equnit::eq_parm (tr/trmetric.f90:30-41) before its eq_load, one more uncounted write for the #260 list.
The Fortran hunks are the C-ABI layer (eq_api.f90) and the registry (eq_param_registry.f90), the same shape as #259 for tr/fp/ti; the eqfile.f90 hunk is a three-line comment and can stay as it is. So no lead confirmation is needed for this one; merge on the maintainers' instruction.
Follow-up of the reviews of #256 and #257, with the counter you proposed on #256 (2026-10-17).
eq_savesees a set of tot'seq:parameterseq_save's precondition was a flag thateq_api_set_paramcleared. tot'seq:dispatcher callseq_param_registrydirectly, so aftertot_set_param("eq:RR", ...)the flag stood andeq_savewrote the parameters as they stood over fields computed for the old ones. As #259 did for tr, fp and ti:eq_param_registrycounts every successful numeric set (eq_param_generation());eq_param_setis a wrapper around the former body.eq_apirecords the count wheneq_run(0)oreq_run(1)succeeds, andeq_saverefuses (EQ_ERR_INVALID, nothing touched) when the flag is down or the count moved. The clear ineq_api_set_paramis gone.MODEFRandMODEFW, excluded in the registry wrapper on the name as the setter reads it. Taking the snapshot again ineq_set_paramafter those two would not see tot'seq:MODEFW, which reaches the registry directly, and would first have to check that nothing else had moved the count; the exclusion covers both callers. And the string sets. fix(tr,fp,ti): re-prep after any registry set, including tot's dispatcher (#253) #259 countstr_param_set_str; for eq that would refuse everyEq.save, which setsKNAMEQjust beforeeq_save.eq_api_set_param_stralready left the flag alone for everyKNAM*.eq_api.hand the eqlib README: tot'seq:parameters are seen now; what no registry sees in the mono image (another module'seq_init/eq_load/eq_calc, aplcommvariable set through another module's registry such asti:RR) points at mono image: sibling entry points and shared plcomm storage change a module's state outside its registry (prepared / has_equilibrium flags cannot see it) #260.The smaller points of the two approvals
savetool: theMODEFWit sets stays set afterwards.eq/eqfile.f90, comment only (upstream Fortran; take it out if it should not go to the lead): the length comparison depends on the run-time, and one that reports the length on disk, or -1, makes it a no-op, not a false failure.test_save.py: a refused write leaves at most the limit (<=), whatever the run-time's buffer size._encode_name's docstring: 80 bytes, not 79.python/_fd_isolation.py: fd 2 is repaired only whenfstatsaysEBADF; another error means fd 2 is there, so it is left alone and the installation fails before changing anything. A host'ssys.stdoutwhoseflushraises anExceptionno longer stops the installation.os.isatty(1)exemption for a server started by hand. A host may run the server on a pseudo-terminal and parse that stream, and the exemption would put the library's output back on it; the READMEs (docs(mcp): READMEs say where Fortran unit 6 output goes under the fd isolation #264) say how to keep that output (2>server.log). Say if you want it anyway, for instance behind an environment variable.Tests
test_save.py: a refused set and a string set leave the save allowed.test_set_after_run.py, on the mono image (CI's mono routing step already runs the file):eq_savewrites aftertot_init,MODEFW=0,eq_run(0)andtot_set_param_str("eq:KNAMEQ")(so tot's string path is shown not to count), refuses aftertot_set_param("eq:RR"), writes aftertot_set_param("eq:MODEFW"). Also checked by hand: throughlibtotapi.so(not only the mono image)eq:RRmakeseq_saverefuse; there is one counter, inlibeqapi.so's registry.test_fd_isolation.py: an fd 2 thatfstatcannot look at is not replaced; a failingflushdoes not stop the installation.develop'seq_api.f90and registry, only theeq:RRcase fails; witheq_saveon the flag alone, the parameter-change case and theeq:RRcase; counting the file modes, four; counting a refused set, the refused-set case; counting string sets, 22test_save.pycases, eq_mcp's real-library save case and two mono cases; each isolation case without its change.macOS, clean build, CI's flags: 975 passed, 41 skipped; with
MONO_LIB_PATHthe mono tests andtest_save.py: 47 passed, 4 skipped (TOTLIB_PATHnot set, no/dev/full).🤖 Generated with Claude Code