- Add tests to tests/unit_parse.py and tests/unit_py_evaluation.py.  These should cover every construct that could be encountered in ICurry.
- fix tests in unit_py_constraints.py
- fix tests in unit_py_pulltab.py
- Come to think of it, I should remove the mutual recursion between D, N, and S.  All of the recusion should be done synthetically.
    - Until then, RuntimeState::stack_exhausted guards the C stack: hnf and
      procN return E_UNWIND at the stack_limit flag, procD rotates the queue,
      and a configuration that cannot proceed within the limit raises a
      "stack limit" error.  A configuration that needs more nesting than the
      limit allows cannot finish; only an explicit evaluation stack fixes that.
- curry.compile seems unable to import Control.SetFunctions
- Documentation:
    - Add information about debugging the C++ backend.
        - Keep the testcase very small.  Reduce it if necessary.
        - Build a unit test and then start the `debug_tests` script.
        - Break at `procD` to get started.
        - Use the gdb command `tty` to direct trace output to another terminal.
        - Use the `str` method to inspect values:
            - Node *:   gdb> p node->str()
            - Cursor:   gdb> p cursor.str()
            - Variable: gdb> p variable.str()
        - To inspect an inductive position relative to the root of a possible
          redex, use the version of `Cursor::str` that takes a variable.  For
          example:

              Cursor redex;
              Variable inductive;
              gdb> p redex.str(inductive);
              head (_a:_b)
                    ~~~~~
                    path: [0], guards: {}

        - By default, `str` substitutes free variables with stylized names _a,
          _b, _c, ...  This can make it difficult to trace free variables
          through steps, as shown below:

              (head (_a:_b), _c)  ->  (_a, _b)

          The variable _c here apparently morphs into _b in the second
          expression.  Pass PLAIN_FREEVARS to any of the `str` methods to avoid
          this.  Free variables will instead be shown with their numeric ID,
          which is fixed.  Using this, the above example might be rendered as
          follows:

              (head (_0:_1), _2)  ->  (_0, _2)




2026-10-02
- readFile on a missing file raises different errors on the two backends: the
  Python backend raises Python's FileNotFoundError from open ("[Errno 2] No
  such file or directory: 'nofile'"), the C++ backend raises an
  EvaluationError ("i/o error: No such file or directory: nofile").  Recorded
  only; the parity gap is not fixed.
- The C++ backend did not report non-determinism in monadic actions.  hnf
  pull-tabbed a choice at the inductive position of any step, so putChar
  ('a' ? 'b') forked and printed both characters, where the Python backend
  raised NondetMonadError.  Only writeFile and appendFile checked for the
  pull-tab and raised the error, with a text of their own ("nondet error:
  file.txt").  Fixed; see the entry below.
- The Python graph copier (backends/py/graph/copy.py) recursed once per node,
  so a cyclic expression raised RecursionError when it was copied, and so did
  a long list value.  Fixed: the copier keeps its own stack and records the
  copy of a node before its successors, as the C++ copier does, so sharing
  and cycles are kept.  The graph equality (equality.py) had the same
  recursion and got the same fix.  Tests: unit_py_copy.py, unit_equality.py.
- Python backend performance.  The evaluator spent most of its time in
  bookkeeping, not in rewrite steps: isinstance checks against abstract base
  classes (inspect.is_boxed, inspect.isa_unboxed_primitive), the visitation
  dispatch in graph/indexing.py, the construction of Variable objects, trace
  bookkeeping that ran with tracing off, and a generator-based context
  manager entered once per step.  Fixed with per-type caches for the ABC
  checks (validated with abc.get_cache_token), one flat loop for indexing,
  __slots__ on Node and Variable, a plain context manager for catch_control,
  and no-op trace helpers when tracing is off.  No semantic change and no new
  flag.  Evaluation time, median of three runs: SomeNum.check 300, 4.49 s to
  1.35 s; naive reverse of 400 elements, 4.02 s to 0.87 s;
  benchmarks/Multgen, 18.40 s to 4.96 s.  The assert in
  Variable.from_logicalpath that indexed the path a second time was removed;
  unit_indexing cross-checks every realpath result.  Tests: unit_py_graph.py,
  unit_py_control.py, unit_inspect.py, unit_indexing.py.
- Non-determinism in monadic actions on the C++ backend (see above).  Fixed:
  hnf reports the error when the redex is monadic (is_monadic), or when
  ($!), ($!!), or ($##) evaluates the argument of a monadic function, as the
  Python _applyspecial does.  The IO built-ins in io.cpp carry F_MONADIC,
  like the generic Prelude table.  Both backends raise "non-determinism in
  monadic actions occurred!".  The text is defined once per backend
  (NONDET_MONAD_ERROR_TEXT in cyrt/builtins.hpp, exceptions.NondetMonadError)
  and unit_io_bind ties them together.  writeFile and appendFile lost their
  own check.  Found on the way: putChar ('a' ? 'b') crashed the C++ backend,
  because ($!) read its argument variable again after hnf had forwarded the
  redex; ($!!) and ($##) with a choice argument failed the source != target
  assertion in pull_tab, because procN scanned the choice as its root (hnf
  runs first now).  The C++ catch applied the handler to the action instead
  of the error value and never cleared the error, so a handler that read
  its argument, or raised an error of its own, aborted the process.  Now
  catch pops the error and hands the IOError value to the handler; an error
  without a value (Prelude.error) becomes IOError msg, as with the PAKCS
  catch.  The C++ error and error2 steps no longer quote the message.  The
  rule still differs between the backends: the Python backend keys it on a
  list of Prelude names (rts_control.is_io) and raises the error on any
  unwind in those steps; the C++ backend reacts to a choice in any monadic
  step, so a user function that calls putStrLn and evaluates a choice is an
  error there and a fork on the Python backend.  Tests: unit_io_bind.py,
  unit_prelude.py.
- C++ backend compile time.  g++ took about 1.0 s for a one-line module
  (-O3; 0.65 s of it in the front end, for cyrt/cyrt.hpp), and PAKCS with
  icurry 4.9 s when the .icy was not cached.  func_eqconstr recompiles its
  767 modules after every libcyrt rebuild, about 770 s of g++.  Measured: a
  lower optimization level saves nothing without a precompiled header (-O0
  0.92 s, -O3 0.98 s) and costs run time (Tak1 and PermSort: -O1 10 to 55
  percent slower, -O0 2.3 to 5 times slower, -O2 equal to -O3); the jq pass
  costs 3 ms per module and 0.09 s on the Prelude, the same as the internal
  encoder.  A precompiled header halves the g++ time (0.45 to 0.5 s per
  module).  Done: Cpp2So precompiles cyrt/cyrt.hpp once per flavor into
  <include>/cyrt/cyrt.hpp.gch/ (SPRITE_CXX_PCH_ROOT relocates or disables
  it) and builds it again when libcyrt.so or a header is newer; make
  uninstall removes the directory.  func_eqconstr on the C++ backend with
  every .so recompiled: 920 s before (as reported), 552 s after; one chunk
  of 143 modules, forced recompile: 165 s before, 117 s after.  The time
  that remains per module is template instantiation for the static data
  (the Metadata maps 0.18 s, the ModuleBOM 0.25 s, the step functions
  0.05 s); extern template declarations do not help, only another
  representation of the module data would.  The Prelude takes 28 s at -O3
  (17 s at -O1, 5 s at -O0), with or without the precompiled header.
  Tests: unit_cxx_toolchain.py.
- C++ collector.  The collector never ran before one billion nodes existed,
  because the nodes Python holds (values handed to Python, expressions built
  with curry.expr) were not roots.  Done: a Node wrapper registers its node
  as a root for as long as it lives (RootHolder in the bindings; gc_add_root
  and gc_remove_root in graph/memory.cpp).  The mark phase also visits the
  error value of a configuration and the queue of every SetEval node it
  meets, which holds the alternatives a lazy set function has not produced
  yet; the old mark phase missed both.  A collection inside a nested
  evaluation (a set function, a step run from Python, an evaluation started
  in a Python callback) sweeps only the nodes allocated since that
  evaluation began and treats the older nodes as roots, because the steps
  of the enclosing evaluation hold nodes in C++ locals (EvaluationScope in
  procD and in the step binding).  The mark phase points a forward node at
  the end of its chain, as compress_fwd_chain does; without that, a
  forwarded root kept every redex of a tail recursion alive.  Dead nodes go
  to free lists by size instead of free(): with free(), the sweep cost
  30 ns per node, and PermSort ran 75 percent slower after its first
  collection, because the evaluation reused scattered chunks from malloc.
  The default threshold is 2^20 nodes; SPRITE_GC_THRESHOLD overrides it,
  and the adaptive policy never shrinks below it (the policy changed; see
  the review follow-ups below).  Measured on the C++ backend with the
  former doubling policy, collector off / at 2^20, evaluation time and the
  peak resident set of the process (VmHWM; the figures given here before
  were VmPeak, the peak virtual size; times vary by 15 percent between
  runs): Tak1 8.8 / 7.2 s and 3.27 GB / 115 MB; Queens10 11.9 / 6.2 s and
  5.20 GB / 115 MB; PermSort 9.3 / 12.5 s and 4.87 / 3.21 GB; ReverseHO
  2.2 / 2.0 s and 1.15 GB / 655 MB; QueensSet9 6.0 / 8.7 s and 3.16 /
  3.16 GB.  2^16, 2^18, and 2^22 are within the noise of 2^20; 2^24 is
  slower on every program (measured with the doubling policy).  A walk over a million list cells (1.5 GB of short-lived
  nodes) completes under a 1 GiB address-space cap, outside and inside a
  set function; with the old threshold both runs end in MemoryError.  The
  process maps 97 MB at load instead of 577 MB (the address vector was
  reserved for the old threshold).  Bindings: gc_collect, gc_node_count,
  gc_root_count, gc_num_roots, gc_collections, gc_seconds, gc_threshold,
  gc_set_threshold, gc_eval_depth.  Tests: unit_cxx_gc.py; the
  out-of-memory program in CxxRuntime.curry now keeps its list reachable.
  Left: the runtime never deletes a Configuration, a Queue, or a Set (the
  3 GB of PermSort and QueensSet9 are mostly those), and the free-variable
  table keeps every free variable and its generator alive, so a program
  that narrows many variables keeps them all.  A raw Arg object held by
  Python is not a root (make_node creates them only for the duration of a
  call).  A set function whose value is the guarded argument itself hits
  assert(0) in procD (T_SETGRD at a configuration root); seen while the
  test program was written, not fixed.  gc/mps.cpp does not define the new
  entry points.  A collection inside a nested evaluation pushes and traces
  every older node, so it costs the whole heap and reclaims nothing from
  before the nested evaluation began (see the header of wdgc.cpp); the
  precise fix is to register the Variable and Cursor locals of a suspended
  step as roots.
- Characters.  The C++ backend stored a Char as a signed byte: ord (chr
  160) was -96, a character above U+00FF could not exist, and a non-ASCII
  character could not be passed from Python (the Arg caster took one byte).
  The Python backend kept code points, but read a file byte by byte, and
  could not compile a string literal with a non-ASCII character (it emitted
  a bytes literal).  Done: a Char is a Unicode code point on both backends.
  unboxed_char_type is char32_t; the Char node keeps its 16 bytes.  Text
  crosses the boundary of the runtime as UTF-8 (cyrt/utf8.hpp): the string
  literals of generated code (_dquote spells the UTF-8 bytes with octal
  escapes; a character literal is a char32_t literal such as U'\xe4'),
  build_curry_string and extract_string, readFile, writeFile, appendFile,
  putChar, getChar, and the display of a cstring node.  A malformed byte
  sequence in a file or on stdin becomes U+FFFD, as in PAKCS.  show writes
  every code point outside printable ASCII as a decimal escape of at least
  two digits ('\228', '\00'), as PAKCS does; the Python show did so below
  256 only.  The literal readers accept every code point (the bound was
  256), and the Python reader accepts uppercase hex digits.  The Python
  backend keeps a string literal as a memoryview of 4-byte code points
  (prelude.string.codepoints), so _biString still slices in O(1).  Found on
  the way: the C++ table of a literal case (ValueSet) held one byte per
  Char, and the runtime read it with an Arg stride, so a free variable at a
  case over two or more character literals read out of bounds; the table
  holds Args now.  ValueBindingsMaker split an odd-sized table as size/2
  and size/2, so a case over three literals lost one value.
  Variable::operator[] dropped the realpath of its parent, so writeFile,
  which head-normalizes the head of its string through such a variable, ran
  the step of the wrong node when the character was not evaluated yet:
  writeFile f (map chr xs) crashed the C++ backend.  The ICurry lexer read
  at most three digits of a decimal escape, so '\128512' became four
  characters.  Tests: unit_chars.py, unit_prelude.py, unit_io_bind.py,
  unit_py_conversions.py, readshow/unicode.curry, io/readFile_utf8.curry,
  io/writeFile_utf8.curry.  Left: the standard streams of the Python
  backend keep the encoding of sys.stdin and sys.stdout (UTF-8 under a
  UTF-8 locale, which the test drivers set).  A lone surrogate from chr is
  written as three bytes by the C++ backend and rejected by the Python
  writeFile.  topython joins a list of strings into one string, so a [Char]
  or [String] value arrives as str; not changed.
- Review follow-ups on the entries above.
  Collector policy: after a collection the threshold is 8 times the
  survivors, but not less than the configured floor, instead of one
  doubling per collection when 15 percent survived.  The heap stays within
  8 times the live nodes (the former bound was about 13).  Measured on the
  C++ backend, former policy / growth 8 / collector off, with the
  evaluation time, the number of collections, the time in the collector,
  and VmHWM: Tak1 7.2 / 6.9 / 8.8 s, 59 / 59 / 0 collections, 115 MB /
  115 MB / 3.27 GB; Queens10 6.2 / 6.4 / 11.9 s, 97 / 97 / 0, 115 MB /
  115 MB / 5.20 GB; PermSort 12.5 / 13.0 / 9.3 s, 8 / 11 / 0 collections,
  0.8 / 1.0 / 0 s in the collector, 3.21 / 3.12 / 4.87 GB; ReverseHO 2.0 /
  2.1 / 2.2 s, 4 / 2 / 0, 655 MB / 839 MB / 1.15 GB; QueensSet9 8.7 / 6.7 /
  6.0 s, 5 / 2 / 0 collections, 2.6 / 0.7 / 0 s in the collector, 3.16 GB
  in all three.  Growth 2 and 4 were slower on PermSort (19.6 and 15.9 s)
  and on QueensSet9 (8.8 and 8.2 s).  On PermSort the time is not in the
  marks, which take 1 s of the 13 s, but in the evaluation after a sweep,
  which reuses the dead nodes in scattered order; returning the dead nodes
  in allocation order did not help (13.5 s).  PermSort and QueensSet9 stay
  slower with the collector than without it because the runtime never
  deletes a Configuration, a Queue, or a Set (the entry above).  That leak
  is the prerequisite for a further gain; the benchmark README notes the
  two programs.  Test: unit_cxx_gc.py.
  Stale C++ caches: the C++ emitter stamps every generated file with its
  format (// FORMAT: 2, compiler.FORMAT_VERSION), and the toolchain starts
  again from the JSON file when a cached .cpp carries another stamp or none
  (Cpp2So.is_stale, Plan.prune_stale).  Before this, a .cpp written by the
  old emitter and newer than its .json.z was compiled against the new
  runtime, which read the one-byte-per-Char table of a literal case with an
  Arg stride.  A .so without its .cpp is still trusted by its timestamp.
  The successor caster maps a Char above U+10FFFF (prim_chr does not check
  its argument; chr clamps) to U+FFFD instead of raising ValueError.  Tests:
  unit_cxx_toolchain.py, unit_plan.py, unit_chars.py.  Found on the way,
  not fixed: the C++ emitter must not run for a module whose shared library
  is loaded in the process.  vIsBuiltin asks the runtime module for the
  symbol, and a loaded library registers every symbol it defines, so the
  emitter declares them all extern and defines none; the library compiled
  from that file fails to load with an undefined symbol.  A first, wrong
  version of the stamp reader did this to the installed Prelude after a
  curry.reload in a test process (the regenerated Prelude.cpp had 0
  definitions and 90 extern declarations).  SPRITE_FORCE_RECOMPILE_CXX
  with a curry.reload can do the same.
  Python copier: an unboxed successor (an int, a float, a char, the
  memoryview of a string literal, the iterator of a generator node) is
  shared, not deep-copied.  copy.deepcopy rejects a memoryview, so a
  partial application that held a string literal could not become a value.
  Tests: unit_py_copy.py, unit_prelude.py (both backends).
  UTF-8 decoder: a sequence cut short, by the end of the input or by a byte
  that cannot continue it, counts as one malformed sequence, and the second
  byte is checked against the range its lead allows.  So both backends give
  the count of U+FFFD that the 'replace' handler of Python gives; the C++
  decoder gave one per byte.  getChar still drops the bytes it read after
  such a lead.  Tests: unit_chars.py.
  inspect caches: the per-type caches of is_boxed and isa_unboxed_primitive
  decide from the type alone and fall back to isinstance for an object
  whose __class__ differs from its type (a proxy, a mock).  Test:
  unit_inspect.py.
  ICurry lexer: the empty escape (backslash, ampersand) is accepted, and the
  seven-digit decimal escape has unit tests (unit_parse.py).
  Tests: unit_io_bind.py and unit_chars.py evaluate the goals of
  data/curry/IOBind.curry and data/curry/Chars.curry, built once, instead
  of compiling a module or an expression per test.  cytest.run_in_subprocess
  sets PYTHONIOENCODING=utf-8 for its children, and func_io compares the
  UTF-8 file as bytes, so neither depends on the locale.  The precompiled
  header has a test for a failed build.
  Monadic non-determinism, a gap on both backends: a choice met by a
  Curry-defined combinator before any monadic step is pull-tabbed to the
  top, so mapM_ putChar ("ab" ? "cd") prints abcd and yields two values.
  PAKCS prints abcd and then reports the non-determinism error.  Recorded
  as an expected failure (unit_io_bind.py, test_mapM_nondet_is_an_error).

2026-10-03: defects found while writing the examples (not fixed)
- curry.save(module, file) without goal= writes a footer that runs nothing
  and ignores -g on the command line; the saved program prints nothing where
  sprite-exec prints the value. examples/04-static-compile is a known failure
  in tests/unit_examples.py for this reason.
  Fixed on 2026-10-05 (issue #35, the fix batch below): save without a goal
  raises ValueError, and the saved program reads -g as sprite-exec does.
- C++ backend: a set function that chooses among sub-terms of its own
  argument (set1 anyOf xs, set1 tails xs, set1 id x, set1 (\xs -> head xs ?
  last xs)) hits an assertion in procD or hnf (fairscheme.cpp) and aborts.
  Choices built from fresh data work. The Python backend accepts every shape.
  Fixed on 2026-10-04 (the set guard at the root, below).
- A free variable created in Python (curry.free()) inside a goal gives a wrong
  value for xs ++ [3] =:= [1,2,3] &> xs, and two of them crash both backends.
  Free variables declared in Curry work.
- show of a Float differs between the backends: "3.5" on the Python backend,
  "3.5000000000000000" on the C++ backend.
  Fixed on 2026-10-05 (the bug batch, below).
- Picking distinct items from a shrinking pool in a recursion (let d = anyOf
  pool in f (delete d pool) (d : acc)) emits its values and then aborts on the
  C++ backend and does not end on the Python backend; structural permutations
  (ndinsert) work.
  Diagnosed on 2026-10-05 (the bug batch, below): a defect of the program.
- Residuation-style arithmetic over free Int variables (the sendMoreMoney
  program of the test data) suspends on the C++ backend and times out on the
  Python backend.
  Fixed on 2026-10-05 (issue #37, below): the toolchain lacked the binding
  optimization that PAKCS applies to every FlatCurry file.
- inspect.getimpl raises ValueError on the C++ backend, and the per-function
  ICurry view prints only "exempt" for some functions (examples/03-inspect).
  Fixed on 2026-10-05 (the bug batch, below).
- A goal without a type signature can come back to Python unevaluated as a
  partial application when PAKCS gives it a class-constrained type.

2026-10-03
- Stale C++ caches (issue #15).  currentfile dropped a .so that was older
  than libcyrt.so (by ctime), so every make stage that relinked the library
  invalidated every object, and the first import after it compiled the
  Prelude again (about 30 s).  Done: each object gets an ABI stamp, a file
  beside it (<module>.so.abi) that holds a digest of the installed runtime
  headers (cxx.toolchain.runtime_digest: SHA-256 over the names and the
  contents of the headers under include/cyrt, 16 hex digits, cached per
  process).  Cpp2So writes the stamp after g++ succeeds and refuses an
  object whose stamp is missing or differs from the digest of the installed
  headers; the plan then compiles the .cpp file again (Cpp2So.is_stale,
  Plan.is_stale, Plan.prune_stale).  The last stage of a plan has no step,
  so the step that made its file answers for it.  The digest leaves out the
  build id of libcyrt.so on purpose: a relink after a .cpp-only change keeps
  the ABI of generated code, and the headers are that ABI.  The source chain
  (.curry, .icy, .json.z, .cpp) keeps the filesys.newer rule.  The
  precompiled header compares its age with the headers only, not with
  libcyrt.so.  SPRITE_IGNORE_CYRT_TIMESTAMP is gone; SPRITE_FORCE_RECOMPILE_CXX
  stays.  Format stamp: the reader scans the first 16 lines and finds the
  stamp on line 3 of a generated file (section comment, import list,
  stamp), so the hazard of the latency analysis (a stamp that is never read
  makes every process compile everything again) was not in the committed
  tree.  Cpp2So now checks its input and raises CompileError when the stamp
  does not read back, so that hazard is an error at the first compile, not
  a silent recompile in every process.  Measured on the C++ backend: a
  second process that imports a module compiled by an earlier process runs
  zero g++ commands (counted through a hook on _system.pexec), the Prelude
  included; the Prelude import costs 0.04 s of CPU.  A make stage that
  changes nothing relinks and copies libcyrt.so (the archive rule touches
  libcyrt.a), which invalidated every object under the old rule; now the
  Prelude object, its stamp, and the precompiled header stay, and the next
  process runs zero g++ commands.  One added comment line in a header,
  followed by make stage, drops the Prelude object and keeps its .cpp file;
  the next import runs two g++ commands (the header member and Prelude.so),
  and the process after it none.  An installation from
  before this change holds a Prelude.so without a stamp, which is compiled
  once more (its Prelude.cpp is kept).  Tests: unit_cxx_toolchain.py
  (TestAbiStamp, the stamp-reader tests), unit_findcurry.py, unit_plan.py.
  Left: a change to the set of builtin symbols in the runtime (a .cpp-only
  change under currylib) changes what the emitter writes for the Prelude,
  which neither stamp sees; raise FORMAT_VERSION for such a change.  A
  comment-only edit of a header invalidates every object; there is no
  switch to trust the objects anyway.
- jq removed (issue #16).  The ICurry-JSON encoder (icurry.json.Encoder)
  writes the compact form itself: no space after a comma or a colon, every
  character outside ASCII escaped.  That is the output of the jq pass
  (--ascii-output --compact-output) byte for byte, and the converter appends
  the newline that jq wrote, so a cache from before this change equals a
  cache from after it.  Gone: config.jq_tool, the --with-jq option and the
  JQ_EXECUTABLE setting of configure, the jq package and option in the CI
  workflow, the pipe and input arguments of _system.pexec, and the external
  icurry2jsontext path of the converter (dead since 2021).  Found on the
  way: IModule stored its imports as a set, so the order of the import list
  in the JSON followed the hash seed of the process, and a fresh conversion
  matched a committed cache only by luck of the seed.  The imports now keep
  the order of the ICurry file (duplicates dropped).  Checked against the
  eight library caches under curry/pakcs-3.4.1: seven were in file order
  already; Data/List.json.z held ["Data.Maybe","Prelude"] and was
  regenerated; all eight now match a fresh conversion under any hash seed,
  the compressed bytes included.  Measured: the conversion of the Prelude
  costs 1.75 s of CPU, almost all in the reader; the jq child cost 0.08 to
  0.09 s of CPU on the Prelude and about 3 ms per small module.  Tests:
  unit_parse.py (EncodeJSON: the compact form, the import order, every
  library cache against the encoder), unit_make.py.  Left:
  src/export/tools/jq.link and the jq filter in src/export/tools/Makefile
  can go.  With no JQ_EXECUTABLE in Make.config the link is skipped at
  install time; the filter never removed it, because Make.rules expands
  LINKS before the filter runs.
- Prebuild in make stage (issue #17).  make stage installed only the .curry,
  .icy, and .json.z files of the Curry library, so the first import after a
  stage generated and compiled the Prelude: about 30 s on the C++ backend;
  and on the Python backend every process compiled the 1.2 MB Prelude.py
  from source (0.2 s), because the loader ran the file through runpy and
  the test drivers run Python with -B.  Done: the library root Makefile
  (curry/Makefile) has a prebuild step that runs after src and the library
  files are installed: sprite-make --py -zc and, when tools/cxx resolves,
  sprite-make --so -zc over the system modules, with CURRYPATH, CFLAGS, and
  CXXFLAGS unset (make exports a polluted CXXFLAGS when the shell set one).
  Without -z the plan looks for .json and regenerates the JSON from the
  installed .icy.  sprite-make --so is new: the plan runs to the shared
  object (MAKE_TARGET_OBJECT); it implies --cxx.  The Prelude compile builds
  the precompiled header member of the default flavor on the way.  The
  Python toolchain step (Json2Py) writes the bytecode cache (__pycache__,
  py_compile) right after it writes a .py file, with or without -B, and
  sprite-make --py writes a missing cache for a file that was current
  already (ensure_bytecode).  The Python loader loads through
  importlib.machinery.SourceFileLoader, which reads a valid cache and
  compiles the source otherwise (and writes the cache unless -B).  --tidy
  removes the cache with the file.  Cpp2So raises a CompileError that names
  the missing compiler when tools/cxx does not resolve, instead of a
  TypeError; the prebuilt objects load without a compiler.  make uninstall
  removes the products.  The root Makefile orders curry after src.  Found
  on the way: sprite-make --py Data.Maybe failed with KeyError('Data'): the
  JSON-to-source step imported the ICurry of a packaged module without its
  package.  The step imports the package first now.  Measured, CPU time in
  new processes after a fresh stage (make unstage, make stage): the stage
  took 43.8 s user and 5.1 s system, most of it the Prelude compile of the
  C++ backend; the first Prelude import 0.032 and 0.027 s on the Python
  backend (0.226 s before, with the source compile) and 0.038 and 0.039 s
  on the C++ backend; zero compiler commands and zero source compiles on
  both (hooks on _system.pexec and SourceFileLoader.source_to_code); import
  curry costs 0.05 to 0.065 s on top.  Reading the Prelude cache costs 3 ms
  against 196 ms for the source (prototype on a copy of Prelude.py).  A
  stage that changes nothing keeps every product (mtimes unchanged).
  Tests: unit_prebuild.py (the staged products of both backends, a first
  import that compiles nothing in a child process, the cache written under
  -B, the loader reading it and compiling again after a change,
  ensure_bytecode, an unwritable cache, --tidy, a module in a package),
  unit_make.py (--so), unit_cxx_toolchain.py (TestMissingCompiler).  Left:
  a generated .py carries no format stamp, so a change to the Python
  emitter does not invalidate a cached Prelude.py (the C++ side has
  FORMAT_VERSION); the prebuild keeps a current .py.  The flavor of the
  precompiled header built at stage time follows the flags of the stage; a
  run with CXXFLAGS set builds its own member at its first compile.  CI
  pays the Prelude compile in the make stage of every job now (about 30 s),
  which the first test import paid before; the py job pays it too.
- Content-keyed ICurry cache (issue #12).  The Curry-to-ICurry cache was off
  by default and could not hit for a dynamic module: its table was named
  after CURRYPATH, which starts with a fresh temporary directory for every
  curry.compile, and its key held the module name, which curry.compile
  numbers per process.  So every unit test paid the front end (about 5 s per
  module; unit_compile.py: 21 calls, 128 s on the Python backend).  Done:
  the key (cache.icurry_cache_key) is a SHA-256 of the module source, the
  sources of the modules it imports (transitively, found as .curry files in
  the Curry path and beside the file; the Prelude is implicit; a module that
  is nowhere, such as a library of PAKCS, adds nothing), the front-end
  options (none today), the icurry tool itself, and the intermediate
  subdirectory, which names the library version.  The file name is not part
  of the key.  The module name is part of it for a named module, because the
  front end writes it into the ICurry; an anonymous module
  (sprite__interactive_N, sprite__expression_N; config.is_anonymous_modname)
  leaves it out, and the cached text is renamed on a hit
  (cache.rename_module: the name follows a double quote or a hash sign and
  precedes a double quote or a dot; verified against the front end's own
  output for another name on a module with instances, a superclass, a
  default method, lambdas, local functions, and an external: 133
  occurrences, equal byte for byte).  A program error of the front end (an
  error at a source position) is cached too and replayed as the same
  CompileError with the new name; a failure of the environment is not
  cached.  SPRITE_CACHE_FILE now names the file and turns the cache on; the
  empty string turns it off; unset, Make.config decides as before.  The test
  drivers (tests/run_tests and the CI script) set tests/.cache/icurry.db,
  and the workflow restores and saves tests/.cache with actions/cache
  (restore keys: the same job, then any job; saved after the tests even when
  one failed).  SPRITE_CACHE_UPDATE matches the Curry source file name.
  The rows live in table curry2icurry_2 (key, name, text, error, created);
  a database error is a warning and the front end runs.  The parsed-JSON
  cache is unchanged.  Found on the way: _system.pexec now attaches
  command, returncode, and stderr to its CompileError.  Measured on the
  Python backend, CPU time of the test process and its children:
  unit_compile.py cold with the cache: 11 tests, 21 front end calls,
  143.7 s user and 42.6 s system (the machine was shared; the same file
  without the cache took 94.6 s user and 33.4 s system earlier in the day,
  21 calls as well); the second run: 0 front end calls, 21 hits (one of
  them the replayed program error), 0.67 s user and 0.09 s system, 0.6 s of
  wall time.  A hit costs about 0.01 s of CPU in curry.compile; the key
  alone costs 2.3 ms (it reads and digests the source and its import
  closure every time: four library modules, 87 KB, for an expression that
  imports Data.List and Control.SetFunctions), and the digest of the icurry
  tool 2 ms once per process.  Tests: unit_cache.py (the rename, the key
  and the import closure, the slot without the front end, the error replay,
  curry.compile hits in the process and in a second process, a named module
  through the toolchain).  Left: the import scan
  is a regular expression over import declarations, lenient on purpose; a
  module present only as .icy or .json.z adds nothing to the key; entries
  never expire (delete the file); the PAKCS version is covered by the
  subdirectory, not by a digest of PAKCS; the old tables of an existing
  ~/.sprite/cache.db stay in the file.
- Bytecode cache for generated Python modules (issue #13).  The prebuild
  entry above covers the Prelude and the system modules: the toolchain
  writes the cache when it writes a .py file, and the loader reads it
  through importlib.  Left open there: a generated file without a cache,
  such as a test module from the overlay archive or from an installation
  made before the cache was written, was compiled from source in every
  process, because the test drivers run Python with -B and importlib writes
  no cache then (the 1,151 generated test modules under tests/data had no
  __pycache__ at all).  Done: the loader writes a missing or stale cache
  (ensure_bytecode) before it loads the file, -B or not; the cache is a
  product of the toolchain, not of an import.  So a generated module is
  compiled from source at most once per change, in any process.  A cache
  that cannot be written gives one warning and the file loads from source.
  Measured on the Python backend, new processes, CPU time: the Prelude
  import costs 0.026 to 0.035 s with zero source compiles (import curry
  0.064 to 0.076 s before it).  unit_py_evaluation.py with a warm ICurry
  cache, CPU of the test process after the imports, whole process in
  parentheses (user plus system): 1.23 s (1.34 to 1.35 s) with the loader of
  the committed tree, which ran the file through runpy and compiled the
  Prelude six times (once per hard reset) and each test module once; 0.26
  to 0.27 s (0.37 s) with the cache of the Prelude alone, the five test
  modules still compiled in every process; 0.32 s (0.43 s) in the run that
  wrote their caches and 0.28 to 0.29 s (0.41 to 0.42 s) after it, zero
  source compiles of cached files.  The five modules are small (1 to 5 KB),
  so their compile cost is in the noise of that file; the gain of the loader
  change is that the larger test modules and the 1,151 modules of the
  functional suites are compiled once instead of once per process.  Tests:
  unit_prebuild.py (the loader writes a stale or missing cache under -B, a
  file without a cache is compiled by the first of two child processes only,
  an unwritable cache gives one warning through the loader).  Left: a
  generated .py still carries no format stamp (see the prebuild entry); a
  read-only tree whose caches are missing warns at every load.
- Run statistics (issue #8).  sprite-exec -t printed the wall time of the
  evaluation at 1 ms resolution and nothing else, and the counters of the
  runtimes (steps_total and gc_collections of the C++ runtime, the step
  counter and the telemetry object of the Python backend) reached no tool.
  Done: sprite-exec --stats prints one line of key=value pairs on stderr
  when the program ends, after its output and after an error message:
  wall (seconds since the process started, read from /proc on Linux, since
  the import of the package elsewhere), cpu (user plus system seconds of
  the process, children excluded), steps (rewrite steps), forks (forks of
  a choice-rooted configuration), collections (the node collector of the
  C++ backend; 0 on the Python backend), peak_rss (bytes, from getrusage),
  and compile (seconds spent in the steps of the toolchain: front end,
  JSON, code generator, C++ compiler; 0 when every file was current).  The
  same numbers come from curry.stats() (Interpreter.stats, module
  interpreter/stats.py): a dict with the seven keys in that order whose
  str is the line.  Steps and forks are per interpreter: the runtime state
  of an evaluation counts its own (steps_total and forks_total on both
  backends; forks_total is new in the C++ RuntimeState, counted in fork),
  and the generic evaluator sums them in EvaluationTotals: a running
  evaluation is held by a weak reference and counted as it goes; an
  evaluation whose generator ends or is dropped is added once.  No hot
  path changed on either backend.  The compile clock sits around the step
  loop of makecurry (outermost call only), so an import that finds every
  file current adds nothing.  The other fields are per process.  Measured
  on mynot (committed caches, no front end): the Python backend reports
  steps=3 forks=1 and the C++ backend steps=4 forks=1 (the backends count
  differently; compare steps within one backend); the first C++ run after
  the header change compiled mynot.so again and reported compile=0.62 s,
  the next run compile=0.000000; cpu about 0.12 s on both backends,
  peak_rss 35 to 37 MB.  Tests: unit_api.py (TestStats: the fields and
  the line, the counts of an evaluation and of a running one, the compile
  field on a hand-written module and on a current one, sprite-exec --stats
  after a run and after an error, no line without the option),
  unit_py_interp.py (testStats: totals per interpreter, kept across a soft
  reset), unit_cxx_gc.py (the collections field follows gc_collections).
  Left: a direct step through evaluator.single_step is added as well, but
  not tested; the harness (M1) is the consumer of the line.
- Benchmark harness v2 (issue #7).  The driver tests/lib/benchmark.py printed
  the best of N wall times of sprite-exec -t to the terminal and stored
  nothing; no suite measured compile time, start-up, or memory.  Done: the
  package tests/lib/benchmarks (benchmark.py forwards to it; ./run_benchmarks
  runs it) with four suites selected by name: throughput (the 30
  dissertation programs; sprite-exec -t --stats, or PAKCS with :set +time),
  compile (each program compiled from source in an empty directory with the
  ICurry cache cold and warm, and the expression 1+2 through curry.compile in
  a child Python), import (python -c pass, import curry, the Prelude import,
  Hello end to end), and memory (the collector on and, on the C++ backend,
  off).  Every measurement runs in a child under timeout and prlimit --as;
  N measured repetitions (5, 3, 5, 1 by suite) follow warm-up runs that are
  not recorded.  One JSON Lines record per item holds the medians of wall and
  CPU seconds (user plus system of the child and its descendants, from
  wait4), peak RSS, the evaluation time alone, the compile seconds, the exact
  counters steps, forks, and collections of --stats (equal in every
  repetition, else null with a warning), the samples, the commit with
  -dirty, the date, a label, and the machine facts.  The compare command
  joins two files on suite, program, backend, and variant, prints the ratio
  of one metric with a noise threshold (same, faster, slower) and the
  counters that changed, and fails under --strict.  Found on the way: Linux
  starts the memory high-water mark of a new process at the resident set of
  its parent, so wait4 on a child of the harness never reports a peak below
  the harness itself (34 MB inside the test process, where python -c pass
  takes 9 MB); the harness runs GNU time inside the wrapper chain for the
  peak, and falls back to wait4 with a note in the record when GNU time is
  missing.  Measured on a shared machine (one repetition, smoke test only;
  the baseline waits for a quiet machine): Hello costs 0.14 to 0.17 s of CPU
  per process on either backend and 1.7 s under PAKCS; repeated records
  agree exactly on steps (1) and forks (0); the warm module compile of Hello
  costs 0.035 s of toolchain time on the Python backend against 5.3 s cold
  (the front end).  Tests: unit_benchmarks.py (the runner with the cap, the
  timeout, a grandchild, and a fat parent; the parsers; the records; the
  suites and their commands; the options; compare on hand-made records; the
  run command end to end on Hello and on the expression).  Docs: tests/README
  section 8 and the README of the benchmark directory.  Left: the nightly
  job with history (M4), the baseline run (M5), a PAKCS column outside the
  throughput suite, in-process repetition for programs under 100 ms (the
  toofast directory stays out of the set), and node counts, which --stats
  does not report.
- Review fixes for the latency items.  ICurry cache: the import scan took
  only names with an uppercase first letter, so the import that curry.compile
  prepends for a dynamic module (import sprite__interactive_N) was not in the
  closure, and the key of an expression left out the source of that module.
  A second process, whose counter gives the same name to another module,
  replayed the expression compiled against the first (probed: show (f 3)
  raised a TypeError after f changed from Int -> Int to Int -> [Int]).  The
  scan accepts any identifier now, and the closure finds the dynamic module
  in the temporary directory that curry.compile puts first in the path.  The
  front end reports a span (line:col-line:col) for a name of more than one
  character; the error matcher took a point only, so most program errors
  were never cached.  It takes the span now.  A missing import ("Interface
  for module M not found") is not cached on purpose: the key sees the Curry
  path only through source files.  Prebuild: sprite-invoke keeps an exported
  SPRITE_HOME, so a make stage run with the variable set compiled the
  library of that installation.  The recipe sets SPRITE_HOME to PREFIX,
  unsets SPRITE_INTERPRETER_FLAGS, SPRITE_CXX_PCH_ROOT, and
  SPRITE_FORCE_RECOMPILE_CXX as well, and fails when Prelude.py (and, with a
  compiler, Prelude.so and its stamp) is missing under PREFIX afterwards.
  C++ toolchain: the format check of a .cpp file belongs to Json2Cpp, the
  step that writes it, so the plan of sprite-make --cxx refuses a stale file
  too (Cpp2So asks the same question; the plan asks a step about its input
  as well, so Json2Cpp answers for .cpp files only).  Cpp2So removes the old
  ABI stamp before g++ runs, so an interrupted compile leaves an object
  without a stamp, not one with the stamp of other headers.  runtime_digest
  raises a PrerequisiteError that names the installation when a header
  cannot be read (a staged link into a source tree that is gone) and gives
  None for a tree without headers; the objects are trusted then, because
  such an installation cannot compile.  Statistics: a step through
  evaluator.single_step (the first step of every compiled expression) was
  never counted on either backend, because the step functions count nothing
  themselves.  Both runtime states have single_step now: the Python one
  passes the root as a variable and counts as S does, the C++ binding counts
  a completed step as procS does.  A compiled expression costs one step in
  --stats on both backends.  CI: the ICurry cache is saved under a key that
  holds the run id, so the nightly run, which alone compiles the functional
  suites, saves as well; the save step is skipped when tests/.cache does not
  exist.  Docs: the wall field of --stats, the configure --cache icurry
  option, the missing-module rule.  Tests: unit_cache.py (the dynamic import
  in the key and across three processes, the span and the missing-module
  errors, lowercase names in the scan), unit_cxx_toolchain.py (the
  source-only plan, the interrupted compile, the header tree),
  unit_prebuild.py (the recipe environment through make print-PREBUILD_ENV,
  sprite-make --py on a current file), unit_api.py (the single step counts
  on both backends).  The long lines of the new tests and of the harness
  were wrapped.  Left: the three-process cache test costs four front-end
  calls (about 20 s); entries of the old point-only error form stay valid.
- Toolchain stream integrated (stream C onto streams A and B).  The
  vendored library under curry/lib, the FlatCurry-to-ICurry port, and the
  front-end route (the default; icurry is the configured fallback) now
  carry the Phase 0 work.  The content-keyed ICurry cache wraps both routes,
  and its key names the route and carries a digest of its program: the
  icurry tool, or the Curry front end with its flags and the sources of the
  port (cache.frontend_digest, icurry_cache_key, Slot(tool=)).  So an entry
  written by one route is never served to the other, and a change to the
  port misses the cache as a new front end does.  The prebuild of make
  stage runs on the vendored layout: sprite-make --json over all 22 modules
  (no run of Sprite writes into the installation for a library module),
  then --py and --so over the modules Sprite can compile; make uninstall
  removes every product, the per-package FlatCurry of the front end
  included; an install of curry/ alone (no sprite-make under PREFIX) skips
  the step with a notice.  Nine modules are left out
  (CURRYLIB_UNSUPPORTED_MODULES in Make.include, exported as
  sysconfig/currylib_unsupported_modules; config.unsupported_syslibs and
  supported_syslibs): seven have externals that no backend implements
  (Handle, IORef, getArgs, getCPUTime, the constants of
  Curry.Compiler.Distribution) or import such a module (Debug.Trace), and
  Test.Prop with Test.Prop.Types, which the importer cannot build: Test.Prop
  is a module and a package and imports its own submodule; the import by
  name walks the prefix Test.Prop and builds it again while its provisional
  module is loaded (the assertion in Json2TargetSource fires), and
  _provisionalModule binds a submodule into an IPackage mapping, which an
  IModule parent is not.  Both backends reject the same nine.  jq is gone
  from configure, the conda recipe, and the docs; the CI matrix keeps the
  icurry key (the py job without icurry, the cxx jobs with it) with the
  cache steps of Phase 0.  Found on the way: the flavor of the precompiled
  header is a digest of the compiler flags, and the include flag followed
  the spelling of SPRITE_HOME; make stage names the installation through
  the link install/ and run_tests through its real path, so the member
  built at stage time was never the one the tests used, and a staged tree
  held two O3 members.  The flag is spelled by its real path now.  The
  committed Control/SetFunctions.json.z under curry/lib predated the import
  order fix of the encoder and was regenerated from its .icy; the other
  seven pinned caches matched.  Measured: a make stage that changes nothing
  keeps all 132 products of the library and the header (mtimes unchanged)
  and runs the front end zero times.  Tests: unit_cache.py (the route in
  the key, the digests of both routes, the calls of either front end),
  unit_prebuild.py (the supported set; the ICurry and JSON of every module),
  unit_currylib.py (the source-only test puts the products and the bytecode
  cache back, in toolchain order, because the staleness rule compares
  ctimes, which a copy cannot preserve), unit_conda.py (no jq),
  unit_cxx_toolchain.py (the real-path include flag).
  Left: the nine unsupported modules; the old overlay directories
  curry/pakcs-3.2.0, curry/pakcs-3.3.0, and curry/pakcs-3.4.1 and the old
  source overlays curry/Control and curry/Data, which nothing references
  now and which can go with git rm.
- Release flags (T1, issue #20).  The optimized build kept its assertions:
  neither Make.config nor the flags of the C++ toolchain said -DNDEBUG, so
  libcyrt.so imported __assert_fail (401 call sites) and so did every
  generated module (3,610 in the Prelude, 45 in Fib); every call between
  translation units of the runtime, and every call from a module into the
  runtime, went through the procedure linkage table (4,188 PLT calls in the
  runtime, 49,582 in the Prelude); and the stack protector was on (313 call
  sites in the runtime).  Done: the optimized flavor in the Make.config
  template of configure adds -DNDEBUG -fno-stack-protector -fno-plt
  -fno-semantic-interposition, and libcyrt.so links with
  -Wl,-Bsymbolic-functions; the debug flavor (make DEBUG=1) keeps the
  assertions.  The flavor reaches the installation as sysconfig/cxx_flavor
  (config.cxx_flavor), and the C++ toolchain compiles generated modules in
  the same flavor (toolchain.FLAVOR_FLAGS: the flags above with -O3, and
  -Wl,-Bsymbolic-functions for every module) unless the interpreter flag
  debug is set.  The ABI stamp is a digest of the headers and of the flavor
  flags now (toolchain.object_digest), so a change to either compiles every
  object again, once: the prebuild of make stage for the library, the first
  import for a cache.  An object of the debug flag carries the debug digest;
  a session without the flag compiles it again, a session with the flag
  keeps it beside the objects of the installation (Cpp2So.accepted_digests).
  The objects depend on Make.config (src/Make.depends), and make warns once
  when configure is newer than Make.config: a tree with an older Make.config
  needs ./configure again.  The assert audit found no assertion with a side
  effect among the 153 of the runtime (the two rv.second patterns already
  cast rv to void), and the build with -DNDEBUG gives no warning.  After the
  change, libcyrt.so and the Prelude import neither __assert_fail nor
  __stack_chk_fail and make one PLT call each (__cxa_finalize).  Measured
  with the harness (C++ backend, three repetitions, CPU seconds, ratio new
  over old; steps equal): Tak1 7.21 to 6.27 (0.87), Fib 1.14 to 1.00
  (0.88), Queens10 6.70 to 5.82 (0.87), Reverse 1.92 to 1.80 (0.94),
  Primes 9.39 to 8.00 (0.85); peak RSS unchanged.  Tests:
  unit_cxx_toolchain.py (TestFlavor: the installed runtime and the Prelude
  have no assertion, the release and the debug flags and stamps, the
  digest, the flavor file; TestAbiStamp follows the new digest).  Left: the
  flavor of generated code is one knob with the runtime's, so a debug
  installation compiles its modules at -O0; CXXFLAGS from the environment
  stays out of the stamp.
- Cheap Variable (T2, issue #21).  Every argument access in a step built a
  Variable with two heap-backed vectors (the path from the root of the step
  and the set guards crossed), so the step of fibgen called operator delete
  18 times and the out-of-line constructor of Variable three times, and a
  nested access (_1[1]) moved the vectors again.  Done: SmallVec
  (cyrt/smallvec.hpp), a sequence with inline room for N plain values and a
  heap block beyond it; Variable keeps its path in a SmallVec of 8 entries
  and its guards in one of 2, and stays 64 bytes.  The heap block comes
  from two helpers of the runtime library (smallvec_alloc, smallvec_free),
  and the growth step is out of line, so a generated module imports no
  allocator for its variables.  The indexer moved from indexing.cpp to
  indexing.hxx, included from node.hxx, so the constructor, operator[], and
  rvalue are inline: a plain successor costs a few loads and one store into
  the inline path; a forward node of one link is shortened in place; only a
  longer chain calls compress_fwd_chain, and only a variable with guards
  calls guarded_rvalue.  operator[] copies the lists of the parent and
  appends, instead of a vector insert at the front; the order of the
  entries is as before (the path of the parent first, the guards of the
  parent last).  The show of the guards prints the set addresses instead of
  "{TODO}".  Found on the way: the front end lifts every level of a nested
  pattern into a function of its own (unwrap__CASE0 ...), so no variable of
  the Prelude or of the benchmarks has a path of more than two entries; the
  inline room is left only through set guards.  Also, with the release
  flags of T1, the known defect of a set function whose value is a sub-term
  of its argument (above, 2026-10-03) is no longer an abort but an endless
  loop: hnf and procD say assert(0); continue; at T_SETGRD.  Measured with
  the harness (C++ backend, three repetitions, CPU seconds, ratio new over
  old; steps equal, peak RSS unchanged): Tak1 6.26 to 3.75 (0.60), Fib 1.02
  to 0.81 (0.80), Queens10 5.84 to 4.58 (0.78), PermSort 12.23 to 11.16
  (0.91), Reverse 1.81 to 1.01 (0.56), Primes 8.35 to 6.06 (0.73), Psort
  38.77 to 23.38 (0.60).  Static: the step functions of Fib.so and Tak1.so
  call no operator new, operator delete, malloc, or free (objdump); their
  call targets are node_reserve, node_commit, compress_fwd_chain,
  guarded_rvalue, the cold grow of SmallVec, and the never-taken
  smallvec_free of the destructor.  Tests: unit_cxx_variable.py
  (TestSmallVec: a C++ program, data/cxx/smallvec_test.cpp, compiled
  against the installed runtime: inline room, growth, copies and moves in
  both directions, self-assignment, append, equality, pointer values, and
  the sizes; TestStepFunctions: the call targets of the step functions of
  Fib and Tak1; TestPatterns: a twelve-level pattern over a lazy and over a
  literal value, two variables of one pattern, the same inside a set
  function, a choice in the argument and one inside, and three nested set
  functions, whose innermost match carries three guards).  Left: W3 of the
  throughput note (plain pointers for pass-through arguments) would remove
  the Variable of an argument that is only passed on; the T_SETGRD loop in
  hnf and procD needs an error instead of assert(0).
- Alias inlining (T3, issue #22).  The Prelude's instance methods for Int
  arithmetic and comparison are aliases: i1 <= i2 = ltEqInt i1 i2, and the
  +, -, and * wrappers forward to plusInt, minusInt, and timesInt.  A call
  of one cost a rewrite step and a node before the call of the external it
  forwards to; four of the ten steps of a call of tak in Tak1 were such
  forwards.  Done: the optimizer pass inline_aliases (interpreter/optimize.py;
  the analysis in icurry/analysis/aliases.py).  An alias is a function whose
  body is one saturated call of another function on exactly its own
  parameters, in order.  The pass runs last, per function, when a module is
  loaded, so it reaches the generated code of both backends and leaves the
  FlatCurry-to-ICurry translation alone (the oracle: 1196 equal, 0
  different).  It replaces each IFCall of an alias by a call of the end of
  the chain of aliases, through the loaded modules; a partial application
  keeps the alias as its head; the alias function itself stays.  Each alias
  records its direct target under the metadata key all.alias_target, which
  both backends write into the module (the C++ Metadata variant takes the
  string), so a module loaded from its compiled form still tells its aliases
  to the modules that import it.  A target may be private to its module
  (both backends resolve a symbol through the symbol table of its module,
  and the C++ backend exports every info table) or external (the call
  becomes the call of the built-in).  When the target lives in a module the
  caller does not import, the module joins the imports, so the C++ link
  line and the Python import list carry it.  A chain that closes on itself,
  a target of another arity, and a target that is not loaded leave the call
  alone.  The optimizers are called as optimizer(interp, ifun, imodule) now.
  Cached generated code: the C++ format stamp went from 2 to 3, and the
  Python emitter got the same stamp (# FORMAT: 2 on line 2 of a generated
  file; Json2Py refuses a file of another stamp or of none, and a file
  without a stamp is format 1), so make stage and the first import write
  every cached module again, once.  The Prelude has 23 aliases; its own
  code calls plusInt from length and ltEqInt from take now.  Found on the
  way: unit_prebuild compared the ABI stamp of the staged objects with
  runtime_digest after T1 made the stamp the object digest; it compares with
  object_digest now.  Also, on the C++ backend a module imported from its
  ICurry object and then by name crashes the process (the registry entry of
  the first import has no library); the new tests use a name per mode.
  Measured with the harness (C++ backend, three repetitions, CPU seconds,
  ratio new over old; steps old -> new; peak RSS unchanged): Tak1 3.68 to
  2.69 (0.73; 49323874 -> 33629915), Fib 0.82 to 0.75 (0.92; 4500007 ->
  3500006), Queens10 4.54 to 4.15 (0.91; 57308654 -> 54505073), Reverse
  1.01 to 1.00 (equal steps), Primes 6.00 to 6.22 (1.04; 61385836 ->
  61364444), PermSort 10.99 to 11.26 (1.02; 14914448 -> 12531154), Psort
  23.06 to 23.37 (1.01; 214193771 -> 214193769).  The values are the same:
  Tak1 gives 16, as an independent computation does; Reverse gives MyTrue
  under PAKCS as well; Fib gives True on both backends; Queens10 gives 724;
  Primes gives 17393, the prime at index 2000.  Tests: unit_optimize.py (the
  alias test on hand-built ICurry, chains, cycles, the arity rule, the
  structure of compiled modules, private and external targets, the import
  that joins, the steps of a call through an alias, a monadic target, the
  installed Prelude), unit_prebuild.py (TestFormatStamp: the Python stamp),
  unit_compile.py (1+2 compiles to plusInt 1 2).  Left: a partial
  application of an alias (map (+ 1), foldr (+) 0, a method reached through
  a dictionary) still goes through the alias, so Fib and Queens10 keep most
  of their wrapper steps; the pass has no switch to turn it off.
- Ownership of configurations, queues, and sets (G1, issue #25).  The C++
  runtime never freed a Configuration, a Queue, or a Set: drop popped the
  front configuration and leaked it, fork leaked the parent of its two
  clones, and the queues and sets of set functions came from new without
  delete.  PermSort and QueensSet9 held about 3 GB, mostly those objects.
  Done: a Queue owns its configurations (state/queue.hpp): pop_front lets
  the front one go, the destructor the rest, and the last queue to hold a
  configuration destroys it (Configuration::holders).  The split of a set
  function's queue on an escaping choice (allValues_step) put one
  configuration object into both queues and let both step it; now both
  queues hold it, and a queue clones it before it steps it
  (Queue::unshare_front, from ready()), so neither sees the steps of the
  other.  The runtime state owns its outermost queue.  The queue of a set
  function belongs to its SetEval node, but a copy of the node shares the
  queue, so the collector frees queues and sets: both register themselves
  (graph/memory.cpp); the mark phase marks the queues on the stacks of the
  runtime states and the queues of the SetEval nodes it reaches, and the
  sets of those queues, of those nodes, and of the SetGuard nodes it
  reaches; the sweep destroys the rest.  A pointer read from a node counts
  only when the registry knows it, because a guard built from Python
  carries a number in the set slot.  allValues_step pops the nested queue
  by RAII, so an error inside a set function leaves the stack of queues as
  it was.  The trace keys its tables by a serial number of the queue, not
  by its address.  Configurations come from a free list, and the scan keeps
  its levels and its call stack in SmallVecs (inline room 4 and 6; SmallVec
  gained resize), so a clone makes no heap call; sizeof(Configuration) is
  376 bytes, in one block.  Found on the way: every collection of
  QueensSet9 ran inside a nested scheduler, where every older node is a
  root, so it marked the whole heap, freed nothing (100 percent survived),
  and raised the threshold to eight times the heap.  A nested scheduler now
  hands E_GC outward as it hands E_ROTATE, and the outermost scheduler of
  the state collects with no step of the state on the C stack; only an
  evaluation started from a Python callback still collects nested.  The
  live configurations also request a collection when they reach an eighth
  of the node threshold, with the same growth rule, because a set function
  consumed only in part (isEmpty) leaves its queue to the collector while
  few nodes are allocated.  SPRITE_GC_GROWTH sets the growth factor
  (default 8).  Bindings: gc_configuration_count, gc_queue_count,
  gc_set_count, gc_queue_lengths, gc_growth; a debug build asserts after
  every collection that the live configurations are exactly those in the
  registered queues, with their holders.  Measured with the harness (C++
  backend; memory suite, one repetition, peak RSS and CPU seconds, old to
  new): PermSort 3036 MB to 466 MB and 10.41 s to 7.73 s (steps equal);
  QueensSet9 3073 MB to 391 MB and 6.82 s to 7.59 s (steps 12140753 to
  12143159 with the collector on, 12141274 to 12144098 with it off; forks
  2979434 to 2978907 and 2979262 to 2979174).  The split changes the
  counters of every program with a choice that escapes a set function:
  before, both queues stepped one configuration object, so a choice made in
  one queue was made for the other; now each clone makes its own choices.
  QueensSet of the throughput suite is such a program: 2172616 steps and
  511344 forks in the baseline, 2152081 and 511413 after G1 and T3
  together (alias inlining changes the steps, the split alone the forks).
  The collection points changed the schedule as well until G2 (below)
  stopped the rotation after a collection: since then QueensSet9 counts
  12144098 steps and 2979174 forks with the collector on and off alike
  (memory suite, measured on 2026-10-04).  Throughput suite, three
  repetitions, CPU seconds: Tak1 2.76 to
  2.45, Fib 0.80 to 0.74, Queens10 4.19 to 3.90, PermSort 10.48 to 7.63,
  Reverse 1.00 to 0.96, Primes 5.97 to 5.52, Psort 22.62 to 21.72; steps
  equal.  Profile of QueensSet9 after the change: the time above the old
  run is in node_reserve after a sweep (the free lists hand out cold
  blocks), in the sweeps, and in the destruction of about a million
  configurations of dead queues.  Tests: unit_cxx_gc.py (TestOwnership: a
  sort by permutation leaves nothing, set functions consumed in part are
  freed during a run, the configuration trigger, nested set functions, an
  error inside a set function, the split; TestSplitValues on both
  backends; test_growth), unit_cxx_variable.py (SmallVec resize).  Docs:
  the environment variables, the benchmark README.  Left: QueensSet9 costs
  11 percent more CPU with the collector on than before, the price of
  freeing memory with this collector (the block heap of the memory note is
  the fix); the Python backend still shares one configuration object
  between the two queues of a split; a collection in a Python callback
  still treats every older node as a root; a Queue still holds its
  configurations in a std::deque, about 600 bytes even when empty.
- Collector stress mode (G2, issue #26).  The collector of the C++ backend
  ran once per 2^20 nodes, so a missing root showed up late or never.  Done:
  with SPRITE_GC_STRESS=1 the request flag stays set (the library sets it
  when it loads, and run_gc sets it again), so check_interrupts returns E_GC
  after every rewrite step and the scheduler collects at every safepoint it
  reaches; a nested set function hands the request outward as before.  The
  hot path is unchanged.  gc_stress() in the bindings reports the mode,
  gc_collections() counts.  The mark stack is kept between collections.  The
  value 0 or an empty value is off; another value is off with a warning.
  Found on the way, on the first run of the unit suite in stress mode: (1)
  hnf_or_free left the residual that its probe of a free variable recorded
  (instantiate adds one) in the configuration, so a step interrupted after
  the rewrite of =:<= returned the nested scheduler to ready() with a void
  residual, and queens of CxxGc.curry ended in a false suspension; a normal
  run meets the same when the periodic rotation lands there, once in 65536
  steps.  hnf_or_free takes the residual back now.  (2) applygnf ($##)
  reported the free variables left in an interrupted normalization, or in a
  constraint that hnf lifted to the root, as residuals, and the
  configuration suspended for good: id $## (let x free in (x =:= 1) &> x)
  suspended in a normal run as well.  The ground check applies only when the
  normalization completed and the redex became the application.  (3) The
  scheduler rotated the queue after every collection (rotate(Q, true)); in
  stress mode that turned a depth-first search into a lockstep over all
  alternatives (the sudoku and cryptarithm examples never finished) and
  starved the sibling in a nested queue (nestedChoice of StackGuard.curry).
  A collection changes nothing in the schedule now: the interrupted
  configuration continues, with its forced flag so that ready() accepts it,
  and check_interrupts counts every compression and checks the rotation
  before the request.  So the stress mode follows the schedule of a normal
  run: the test programs give equal values, in the same order, with equal
  step counts.  A test whose child cannot end in stress mode skips there
  (cytest.skipIfGcStress: the out-of-memory program, the 200000-element
  value, the deep recursion, the threshold and growth policy tests;
  unit_examples leaves out the two search examples).  Measured: Tak0 in
  stress mode takes 22.7 s against 0.72 s, with 9350060 collections for
  9350060 steps and a peak RSS of 35 MB against 122 MB; the throughput suite
  is unchanged (ratios 0.94 to 1.08, counters equal).  The unit suite in
  stress mode on this machine: 46 files green, one process per file, about 5
  minutes warm.  CI: the job stress of ci.yml runs the unit suite on the C++
  backend in stress mode on the nightly schedule and on request (input
  stress); the push jobs are unchanged.  Tests: unit_cxx_gc.py (TestStress:
  collections per step, the same values and steps as without the mode, a
  nested callback evaluation, off unless asked; the base class ChildTests,
  so the inherited tests run once), unit_examples.py (STRESS_EXCLUDED),
  unit_cxx_stack.py and unit_cxx_runtime.py (the skips).  Docs: the
  environment variables, tests/README.  Left: the Python backend has the
  same probe in hnf_or_free (fairscheme.py) and no stress mode; test_convert
  of unit_curry2icurry fails whenever the ICurry cache holds its modules
  (the front end does not run, so no .fcy file appears), with or without the
  stress mode; a nested scheduler that hands E_ROTATE or E_UNWIND outward
  from a queue of one configuration leaves the front without the forced
  flag.

2026-10-04
  The peak RSS values of the entries below come from the benchmark harness,
  whose compare command prints bytes / 1048576 with the suffix M: the unit
  is MiB where an entry says MB.
- Scheduler counters (M4, issue #10).  The parallelism gate asks how much of
  the work of a program the queue could run in parallel, and nothing
  measured it.  Done: make COUNTERS=1 adds -DSPRITE_SCHEDULER_COUNTERS (like
  TRACE=1; the objects do not depend on the variable, so touch Make.config
  to switch).  The runtime then counts per evaluation (state/counters.hpp):
  the steps taken while the outermost queue held one configuration, the
  steps inside set functions, the steps whose redex another configuration
  created, the largest outermost queue, and the lifetime of every
  configuration that ends by a value, a failure, or a fork (a histogram,
  exact below 1024 steps, one bucket per power of two above), for the
  outermost queue and for the nested ones.  The creator of a node is the
  serial number of the configuration whose step allocated it; node_reserve
  writes it into the word before the node, and procS compares it with the
  stepping configuration.  So "shared" is work on a node that an ancestor
  made before a fork or that a sibling made: an upper bound of what a design
  with one copy of the graph per alternative would duplicate.  The flag
  changes the runtime and the bindings only: the new fields are last in
  RuntimeState and Configuration, node_reserve is out of line, and
  generated code reads neither, so the staged modules serve both builds.
  The bindings hand the raw counters to Python (RuntimeState.
  scheduler_counters, scheduler_counters_enabled); EvaluationTotals sums
  them over the evaluations (the maxima take the largest), and curry.stats
  and sprite-exec --stats append fourteen keys after compile (stats.
  SCHEDULER_KEYS: serial_steps, nested_steps, shared_steps, queue_max,
  configurations, failures, failed_steps, lifetime_median, lifetime_mean,
  lifetime_max, and the nested four).  The harness accepts the longer line
  and keeps it in extra.stats; run_benchmarks counters FILE prints one row
  per program with the serial, nested, shared, and failed fractions.  On
  the way: Queue keeps its size in a counter (the size of a deque is an
  arithmetic over its iterators, and count_step asks at every step), and
  single_step of the bindings steps the configuration the evaluator made
  instead of a second one for the same root.  Measured, the plain and the
  instrumented runtime side by side under perf stat (the machine had
  another load; instructions are the stable number): Tak1 26.39 to 27.23 G
  instructions (+3.2 percent), cycles +6 to +8 percent; PermSort 29.68 to
  30.24 G (+1.9), cycles +3 to +5; QueensSet 9.21 to 9.30 G (+1.0), cycles
  within the noise; peak RSS +8 to +20 percent (the word per node).  The
  5 percent budget holds for PermSort and QueensSet in cycles and for all
  three in instructions; Tak1, which allocates most, misses it by the
  cache misses of the larger heap.  The 30 dissertation programs (one
  repetition each, the file in the measure directory of the build tree):
  the sixteen programs without a fork have a serial fraction of 1 and one
  configuration; twelve search programs have a serial fraction below 0.07
  (Half, SearchMAC, ColormapChoice, ColormapFree, Horseman, Last,
  PaliFunPats, PermSort, PermSortPeano, QueensSet, RegExp, SearchQueens),
  a median lifetime of 0 to 8 steps (mean 2 to 33), and a shared-work ratio
  of 0.10 (PermSortPeano) to 0.99 (ColormapChoice); QueensSet runs 97
  percent of its steps inside set functions.  PokerChoice and PokerFree do
  not compile here (the front end wants currypp), as in the baseline.
  Tests: unit_cxx_counters.py (the median and the sum in Python on every
  backend; on an instrumented runtime the exact counters of count, coinNot,
  oneFails, twoValues, inSet of data/curry/CxxCounters.curry and of psort
  and countQueens of CxxGc.curry, the invariant that every step belongs to
  one configuration of the outermost queue, a configuration left in the
  queue, a single step, the sum over evaluations, the sprite-exec line; on
  a plain runtime the seven fields alone), unit_api.py (the keys after the
  seven), unit_benchmarks.py (the parser, the counters command).  Docs: the
  sprite-exec page, tests/README, the benchmark README.  Left: the gate
  reading: the search programs have the parallelism in the queue but each
  configuration lives a handful of steps and most of its steps are on
  shared nodes, so a worker must own a subtree and the graph must stay
  shared; the shared counter tags every node, so a build that wants the
  serial fraction and the lifetimes alone pays for the word too (a second
  level of the flag would split it); the nested lifetimes miss the
  configurations still in the queues of set functions, which hang off
  SetEval nodes.
- Plain-data module format (L5, issue #18).  With the precompiled header a
  small module still cost g++ about 0.3 s for its static Metadata maps
  (std::unordered_map of std::variant) and its ModuleBOM aggregate
  (std::string, std::vector, std::map, std::tuple): the instantiation of the
  templates and the code of the dynamic initializers.  Done: the generated
  code spells the bill of materials as static arrays of plain records (new
  header cyrt/bom.hpp, namespace cyrt::bom: Entry, Metadata, Alias, Type,
  Function, Module; every member is an integer, a pointer, or a string
  literal) and exports one record, _bom_.  The record is declared extern
  inside the extern "C" block, because a const object at namespace scope has
  internal linkage otherwise.  The compiler writes the arrays as data: no
  template is instantiated for them and no constructor runs at load.  The
  loader (SharedCurryModule) reads the record, refuses one of another layout
  version (bom::VERSION; the DynloadError names both versions; a library
  without the record and a record without a name are errors too), and
  decodes it into the ModuleBOM the runtime and the bindings read
  (ModuleBOM::decode).  The registry entry owns the decoded object.  A
  metadata record shared between symbols is decoded once (the Prelude shares
  36 records among about 1,700 symbols).  Module::link, loader.py, and the
  Python view of the bill of materials are unchanged.  FORMAT_VERSION 3 -> 4:
  a format-3 file no longer compiles against the new headers (the first
  member of ModuleBOM is now the deque of the decoded metadata), and the
  stamp keeps it from the compiler.  The ABI stamp is unchanged; the header
  change recompiles every object once.  The emitter refuses a metadata value
  that is not a str, an int, or a bool, and an int outside the C++ range,
  with a CompileError that names the key (g++ failed on such a value
  before).  The symbol kind MODULE_DATA ('T') names the tables.  Measured on
  the C++ backend: g++ on Fib.cpp with the precompiled header 0.62 -> 0.35 s
  user (0.73 -> 0.44 s wall); -ftime-report: opt and generate 0.55 -> 0.27 s,
  template instantiation 0.20 -> 0.15 s.  Peano takes 0.54 s user (0.64 s
  wall) after the change: its 15 step functions cost 0.47 s in opt and
  generate at -O3.  The Prelude 27.4 -> 26.5 s user (its 3,194 step
  functions take almost all of it).  Compile suite (-r 3, CPU seconds of the
  whole process): Peano warm 1.230 -> 0.855 (0.70), Fib warm 0.918 -> 0.642
  (0.70), cold 0.83 and 0.79; the compile field: Peano warm 1.103 -> 0.737
  (0.67), Fib warm 0.793 -> 0.530 (0.67).  func_eqconstr.py on the C++
  backend with every module regenerated and compiled again (767 programs;
  CPU seconds of the test process and its children; five chunks of the
  file, one process each): 569.5 -> 318.8 s (0.56).  Throughput (seven
  programs, -r 3): every item the same within 3 percent, counters equal;
  the import suite the same.  Static evidence: the module objects (the test
  module and the staged Prelude.so) import no operator new or delete, no
  _Hashtable, and no basic_string; before, every module imported operator
  new for its maps.  Tests: unit_cxx_bom.py (the decoded bill of materials
  of data/curry/CxxBom.curry, with a metadata value of each kind and a
  private function; the generated file; the undefined symbols of the
  objects, the Prelude included; the loader's checks on hand-compiled
  libraries; the emitter's errors).  Left: g++ now spends its time on the
  step functions (code generation at -O3 and the instantiation of the inline
  indexing, about 0.15 s per module), so a module of Peano's size stays
  above 0.4 s; the InfoTable constructor is out of line, so every static
  info table is initialized by a call at load (1,641 calls for the Prelude);
  an aggregate InfoTable would make the tables data as well; the C++
  DynloadError reaches Python as RuntimeError, not as
  curry.exceptions.DynloadError.
- Review of the throughput stream: the set guard at the root and the
  interrupted step (issues #20, #21, #25, #26 follow-up).  (1) A set function
  whose value is a sub-term of its guarded argument (set1 id x, set1 anyOf
  xs, set1 tails xs, set1 (\xs -> head xs ? last xs); the defect of
  2026-10-03) put the guard of the argument at the root of the nested
  configuration, or at the inductive position of a step after a rewrite.
  procD and hnf said assert(0); continue there: the debug flavor aborted,
  the release flavor of T1 looped (isEmpty (set1 id 1) spun until the cap
  killed it).  Done: procD treats a guard at the root as a node of the
  spine, like a constructor.  procN descends into it, and a choice below it
  is pulled up through it, which puts the choice into the escape set of the
  guard's set (Scan::copy_spine), so the choice escapes the set function;
  make_value drops the guards of the set from the value.  hnf crosses a
  guard at the inductive position as the indexer does (Variable::skip): the
  set joins the guards of the variable and the guarded expression becomes
  the target, so rvalue puts the guard back and a choice below it joins the
  escape set.  allValues_step accepted only a constructor as a value and
  took anything else for the escaping choice; a free variable, and a term
  under the guard of an enclosing set function (the inner of two nested set
  functions returns its argument, which carries both guards), are values
  too.  The C++ backend gives the values of the Python backend for every
  shape of the defect, and two values, [[1]] and [[2]], for sortValues
  (set1 inner (1 ? 2)) with inner x = sortValues (set1 id x), where the
  Python backend fails an assertion (allValues expects a choice when D
  unwinds for a guard of another set).  (2) hnf_or_free took back the
  residual of the variable its probe recorded, but not that of the
  variable's group (add_residual records both): after y =:= x, the probe of
  x in x =:<= e left the group's residual in the configuration until the
  constraint reached the root.  And a nested scheduler that yielded E_ROTATE
  or E_UNWIND from a queue of one configuration set no forced flag (the
  E_GC path did).  Together, a periodic rotation or the stack limit inside
  such a step made the nested ready() find one void residual and throw
  EvaluationSuspended for the whole goal.  Reproduced on the C++ backend: a
  set function of one configuration beside a long alternative, the
  constraint under 3000 strict applications (the window of the
  interruption), and a preamble whose length puts the 65536th forward node
  inside the window (two of 55 lengths scanned suspended); and a stack
  limit of 64 KiB inside x =:= deep (a suspension instead of the stack
  error).  Done: Configuration::remove_residual erases the variable and its
  group, and both yields set the forced flag, as E_GC does.  Found on the
  way: in a deterministic run the nested scheduler calls ready() once and
  then not before an interrupt or a choice, so the residuals of completed
  steps pile up in the configuration until then; they are bound by then
  and hide a void one, which is why the suspension needs the interrupt in
  the first such step after a ready().  Measured with the harness (C++
  backend, three repetitions, CPU seconds, before to after; steps, forks,
  and peak RSS equal): Tak1 2.57 to 2.59 (1.01), Fib 0.73 to 0.76 (1.05),
  PermSort 7.92 to 8.35 (1.05), Queens10 3.84 to 3.94 (1.03); the changes
  are off the hot path (a case label, a rare branch, and two erases), and
  under perf stat the run after takes 24.49 G instructions for Tak1, 4.81 G
  for Fib, and 24.24 G for PermSort, below the counts of the M4 entry
  (measured under another load).  Against the
  baseline of 2026-10-03: Tak1 6.87 to 2.59, Fib 1.06 to 0.76, PermSort
  13.22 to 8.35 (peak RSS 3181 MB to 490 MB), Queens10 6.62 to 3.94,
  QueensSet 1.30 to 1.11 (578 MB to 145 MB; steps 2172616 to 2152081,
  forks 511344 to 511413, see the G1 entry).  Tests: unit_cxx_scheduler.py
  with data/curry/CxxScheduler.curry (TestSetGuardRoot on both backends:
  the values of the shapes above, the escaping choice of the argument, a
  free variable as the argument; TestNestedSetGuard: the nested set
  functions, and the captured argument as an expected failure;
  TestNestedSetGuardPython: the nested case as an expected failure of the
  Python backend; TestInterruptedStep: the stack limit and the rotation
  inside the constraint, the latter a scan of 55 preamble lengths over one
  rotation period, skipped in the stress mode of the collector).  Without
  the fixes the two interruption tests fail with EvaluationSuspended, and
  the set-guard children time out.  Left: a choice of the argument of an
  enclosing set function that the inner function captures (not receives)
  carries the outer guard only, so the inner set function encapsulates it,
  on both backends (test_captured_argument); the Python backend fails an
  assertion on a guard of an enclosing set function at the root of a nested
  configuration; escape_all of the C++ Configuration is no longer set
  anywhere.
- In-place rewrite on the C++ backend (T4, issue #23).  Every return of
  generated code and every boxed primitive result did _0->forward_to(new
  node): it allocated the result and left a forward node in the redex, and
  every consumer compressed the chain and dispatched again.  The Python
  backend rewrites in place (node.py rewrite) and forwards only a reference
  result.  Done: Node::rewrite(info, args...) writes the info table and the
  successors of the result into the redex and returns the tag of the result;
  the arguments are read into locals before the first slot is written,
  because an argument may be a successor of the redex (swap x y = (y, x)).
  The generator (vEmit_compileS_IReturn) emits it when the block of the
  result fits the block of the redex, both computed from the arities as the
  info tables spell them (sizeof(Head) + sizeof(Arg[max(1, arity)]);
  InfoTable asserts the lower bound), and _0->forward_to as before when the
  result is larger, a partial application, or a pinned nullary constructor
  (Nil, Unit, True, False, Fail: the collector takes a node with a pinned
  info table for the static object and would never mark a heap copy).  A
  literal result is written as its info table and value, not as a new node.
  A reference result goes through Node::forward_or_copy: the redex is
  forwarded to the node, unless the node is an Int, Char, or Float, whose
  two words are copied (such a node is never rewritten, so a copy is as good
  as the node).  The primitives of unboxed.def write their boxed result into
  the redex; a Bool result is pinned and keeps the forward node.  apply
  writes the function node of a completed partial application into the
  redex when it fits (a function of up to two arguments;
  Node::rewrite_from_partial shares its filler with from_partial).  The
  rotation and collection cadence moved from the forward-chain compressions
  (the T_FWD cases of procD, procN, and hnf) into procS: a completed step
  counts and calls check_interrupts, so stepcount counts steps, the
  rotation runs every 65536 steps, and the stress mode of the collector
  collects after every step as before.  E_RESTART passes the check
  unchanged: it is counted by every enclosing procS on its way out, and a
  replacement by E_GC at the innermost level dropped those counts in stress
  mode (queens of CxxGc.curry counted 3068 steps against 3500).  hnf follows
  a chain of in-place tail calls under one scan frame (a do-while on
  T_FUNC).  The step binding of the Python API (InfoTable.step) refuses a
  node of another info table, because the step may write into the block.
  Audit of cursors into the redex across a rewrite: hnf still forwards its
  own results (a failure, a lifted constraint, a pull-tabbed choice), so
  the check in _applyspecial (_0->info->tag == T_FWD) stands; the trace
  prints the cursor of the slot that holds the redex, not a slot inside it,
  and its "same expression" rule compares the address at the end of the
  forward chain, which is the redex itself after an in-place rewrite; the
  set-function steps keep forward_to (their results do not fit); the
  counters build names the redex by its address, which an in-place rewrite
  keeps.  FORMAT_VERSION 4 -> 5, so cached generated files are written
  again once.  The collector counts the nodes allocated since the start
  (gc_num_allocations, gc_allocation_count in the bindings), for the nodes
  per step.  Found on the way: the overlay archive of this worktree was
  extracted after the checkout, so the stale ICurry of five test modules
  (CxxBom, CxxCounters, CxxGc, CxxScheduler, CxxVariable; the archive
  predates their last change) was newer than their sources, and the
  regenerated CxxGc lacked psort and escaped; the sources were touched so
  the front end runs again.  Measured with the harness (C++ backend, three
  repetitions, CPU seconds, before to after, ratio; steps equal unless
  said): Tak1 2.61 to 2.48 (0.95), Fib 0.74 to 0.64 (0.86), Reverse 0.97 to
  0.84 (0.87), Queens10 3.84 to 3.51 (0.91), PermSort 8.36 to 7.65 (0.92),
  QueensSet 1.14 to 1.01 (0.89; steps 2152081 to 2152979 and forks 511413 to
  511583, because the rotation now counts steps, not compressions; the 92
  solutions equal those of the Python backend), Primes 5.60 to 4.70 (0.84).
  Peak RSS (memory suite, collector on): Fib 233 MB to 170 MB, Reverse 100
  MB to 84 MB, Tak1 and PermSort unchanged; collector off: Reverse 1060 MB
  to 547 MB, PermSort 2299 MB to 1711 MB.  Nodes allocated per step
  (collector counter over an evaluation through the API): Tak1 1.400 to
  1.133 (-19 percent), Fib 2.143 to 1.429 (-33), Reverse 1.999 to 1.001
  (-50), Queens10 1.816 to 1.398 (-23), Primes 2.066 to 1.599 (-23),
  PermSort 2.701 to 2.542 (-6), QueensSet 1.895 to 1.824 (-4).  The
  acceptance of the plan (Tak1 nodes per step -40 percent, Tak1, Fib, and
  Reverse -25 percent CPU) is not met.  Tak1 after T3 allocates, per call
  of tak that recurses, the case function (one slot larger than tak, so it
  does not fit), the comparison, three calls, three differences, three
  literals, and three boxed results; the rewrite removes the three boxed
  results and the outer call, 14 to 11 nodes per recursing call (a base
  call allocates two either way), and the literals are T5.
  perf on Tak1 after the change: 22.9 G instructions at 2.42 per cycle; the
  two generated steps take 40 percent (the hottest instructions store
  Variable objects on the stack), node_reserve 18, the collector 8, hnf 7,
  Variable::skip 6, Scan::push 4, compress_fwd_chain 0.5: the forward nodes
  were cheap on Tak1 after T2, and the allocation of the call nodes, the
  Variable objects, and the nested evaluation are what remains (T5, T6, G4).
  Tests: unit_cxx_rewrite.py with data/curry/CxxRewrite.curry (the values
  on both backends; one step at the root of a raw expression for each shape
  of result: a tail call, a constructor, a larger result, a pinned result, a
  nullary constructor of the program, a literal, a reference to a primitive
  and to an expression, swapped and rotated arguments, apply; a walk by tail
  calls allocates no node per element; the rotation and the collections
  without forward nodes, in children; the generated code of each shape) and
  data/cxx/rewrite_test.cpp (Node::rewrite with swapped successors, a
  nullary result, a smaller result in a larger block and its copy,
  forward_or_copy on a primitive, a pinned node, a function node, and
  through a Variable, rewrite_from_partial; compiled with assertions).
  Left: a result one slot larger than the redex, the lifted case of every
  if and of every case on a literal (f args = f_COMPLEXCASE0 args
  scrutinee), still forwards; one spare slot per function node would turn
  that into a rewrite at eight bytes per function node.  A Bool result and
  the other pinned constructors keep the forward node; the collector would
  have to tell the static object apart by its address before a heap copy of
  a pinned node is allowed.  A partial application as a result keeps the
  forward node.  The nodes per step are not in --stats or in the harness
  records.  The front end lowers a case on an Int literal to ==, so such a
  case always costs the lifted function and a forward.
- Literal and small-value tables on the C++ backend (T5).  Generated code
  allocated a boxed literal on every execution of a step (Tak1 three per
  recursing call, Fib two, the Prelude's length one per element).  Done: the
  runtime keeps one node for each integer from -128 to 1023 and for each
  ASCII character (g_small_ints and g_small_chars in builtins.hpp), made
  when the library loads.  The literal nodes live in an arena of their own
  (literal_reserve in graph/memory.hpp, one block of 16 MB committed as it
  is touched), outside the heap of the collector: the mark phase skips them
  by their address, and the sweep never sees them, so a collection costs
  nothing for them.  A first version registered them as roots, which the
  stress mode exposed: a collection on a heap of two live nodes took 4.9 us,
  all of it for the 1,292 roots, and the walk of a million cells inside a
  set function in unit_cxx_gc.py no longer finished in its 120 s.  int_ and
  char_ return the shared node of a value in range and allocate otherwise;
  float_ is unchanged.  The debug flavor asserts in Node::forward_to,
  rewrite, forward_or_copy, and rewrite_from_partial that the node written
  is not a literal node (gc_is_literal).  A literal
  outside the tables is a node of its module: the generator
  (internLiteralNode; symbol kind LITERAL_NODE, code L) defines
  static Node * const CyL... = literal_node(info, value) once per spelling
  in the .strings section, and the module makes the node when it loads.  A
  literal inside the tables stays int_(1) or char_(U'a'), which the C++
  compiler folds to a load from the table.  A literal that is the result of
  a step is still written into the redex.  FORMAT_VERSION 5 -> 6.  Why one
  node per value is safe: a node of a primitive value is never written (a
  rewrite targets a redex and a forward a redex or a free variable;
  forward_or_copy copies from the shared node into the redex), the bindings
  compare primitive values by value (add_binding reads the value word
  through obj_id), the repr form marks only a cycle with an ellipsis, and
  the Python binding Node.forward_to now refuses a primitive value.  New in
  the bindings: SMALL_INT_MIN, SMALL_INT_MAX, SMALL_CHAR_MAX, small_int,
  small_char, gc_literal_count, gc_is_literal.  Found on the way: ICurry
  spells a negative
  literal as negate applied to a positive one, so generated code never
  spells one.  Measured with the harness (C++ backend, three repetitions,
  CPU seconds, before to after, ratio; steps and forks equal): Tak1 2.51 to
  2.20 (0.87), Fib 0.66 to 0.61 (0.92), Reverse 0.86 to 0.87 (1.02),
  Queens10 3.55 to 3.42 (0.96), PermSort 8.06 to 7.35 (0.91), QueensSet
  1.06 to 0.99 (0.93), Primes 4.88 to 4.48 (0.92).  Nodes allocated per
  step: Tak1 1.133 to 0.933 (-18 percent), Fib 1.429 to 1.143 (-20),
  Queens10 1.398 to 1.364, QueensSet 1.824 to 1.814, Primes 1.599 to 1.565,
  Reverse and PermSort unchanged.  Peak RSS with the collector off: Tak1
  2292 to 1710 MB, Fib 302 to 232 MB; with the collector on, Fib 170 to 195
  MB: its two collections fall later in the run (fewer nodes per step), so
  more of the growing list is live and the adaptive threshold allows a
  larger heap afterwards; with the threshold scaled by 0.8 the peak is 168
  MB.  The other items are unchanged.  Tests: unit_cxx_literals.py with
  data/curry/CxxLiterals.curry (the values and the show forms on both
  backends; the bounds through the bindings; the successors of stepped
  expressions are the shared nodes; one node per literal of a module; the
  literal count after a module loads; the nodes survive collections; a
  loop that spells literals allocates three nodes per iteration, not five;
  the bindings refuse to forward a value; the generated code) and
  data/cxx/literals_test.cpp (int_, char_, float_, build_curry_string,
  literal_node and gc_is_literal across run_gc, forward_or_copy from a
  shared node); the reclaim bound of test_value_survives_collections in
  unit_cxx_gc.py now leaves out the Int and Char nodes of the table, which
  are shared.  Left: the entries -128 to -1 serve only the runtime's own
  calls; Node::create from Python allocates an Int node per call
  (curry.expr(1) is a fresh node); a string literal stays a _biString node
  per execution, because its step rewrites it into a list; the literal
  nodes of a module are never freed, as modules are never unloaded; the
  arena is one block, so a process with more literal nodes than it holds
  (about a million) fails at the load of the module.
- Plain pointers for pass-through arguments on the C++ backend (T6).  A step
  built a Variable for every argument it read: the slot, the path from the
  redex, and the set guards crossed; after T5 the hottest instructions of
  the two steps of Tak1 stored those objects on the stack.  An argument that
  the step only passes on to another call needs none of that: nothing
  head-normalizes it from the step (the path), and the value it denotes is
  the node in its slot (the guards).  Done: the generator reads such an
  argument as a plain Node * (the rule is in backends/cxx/passthrough.py).
  A variable is plain when it is declared by IVarDecl, assigned once from a
  successor of the redex or of another variable with a path of one entry,
  from a node the step builds, or from another plain variable, and every use
  passes the value on: an argument of a call, of a partial application, or
  of a choice, the expression of a return (forward_or_copy), or the right
  side of the patch of a recursive let.  A variable that a case scrutinizes
  keeps the Variable (hnf needs the path for the scan and the guards for the
  escape set of a pull-tabbed choice); so do the base of a path and the base
  of a node assignment; the two variables of a var-to-var assignment have
  one kind.  The reads: Node::successor_node(i) (graph/node.hxx) gives the
  node in slot i of the redex, skips a forward node there and shortens the
  slot as the indexer does, and keeps a set guard in the slot, because the
  redex is a function node, never a guard, so the slot holds the whole
  guarded argument (the rvalue of a Variable built the guard again, one node
  per use).  A successor of a Variable (a pattern variable) may lie under
  the guards the Variable crossed, which the ICurry does not show, so
  Variable::successor_node(pos) (graph/indexing.hxx) decides at run time:
  the plain read when the Variable crossed no guard and its target is a
  constructor, the indexer and rvalue otherwise.  A path of more than one
  entry keeps the indexer.  A plain variable starts null (Node * _3 =
  nullptr), as an unassigned Variable yields null: a recursive let builds a
  forward reference from a variable not yet assigned (cyc in
  CxxPassThrough.curry: _2 = Cons n _3 before _3 is assigned, then
  _2.set_successor(1, _3)).  FORMAT_VERSION 6 -> 7.  Static evidence: the
  step of tak in Tak1.so and fibgen in Fib.so call node_reserve,
  node_commit, and compress_fwd_chain only; no smallvec_free, no Variable
  destructor, no guarded_rvalue (objdump).  Measured with the harness (C++
  backend, three repetitions, CPU seconds, before to after, ratio; steps,
  forks, and peak RSS equal): Tak1 2.20 to 1.76 (0.80), Fib 0.64 to 0.58
  (0.90), Reverse 0.88 to 0.67 (0.75), Queens10 3.47 to 3.04 (0.88),
  PermSort 7.84 to 7.51 (0.96), QueensSet 1.06 to 1.06 (1.00), Primes 4.72
  to 3.94 (0.84); the memory suite the same for every item.  Nodes per step
  unchanged (QueensSet allocates two nodes fewer of 3.9 million: a shared
  guard instead of a rebuilt one).  Tests: unit_cxx_passthrough.py with
  data/curry/CxxPassThrough.curry (the values on both backends: arguments
  in another order, an argument returned, passed to a partial application
  and to both sides of a choice, a pattern variable, a slot that holds a
  forward node, a recursive let, a free variable beside a plain one, and
  set functions whose function passes its guarded argument on, into a call,
  a second call, a constructor, and through a pattern match, with the
  choice outside and inside; the analysis on hand-built ICurry; the
  generated code of each shape; the call targets and the text of the steps
  of tak and fibgen) and data/cxx/successor_node_test.cpp (the reads on
  hand-built nodes: a chain of two links shortened, a guard kept, a null
  slot, a variable with and without a guard, and one whose target became a
  forward node).  Left: a path of two or more entries (none in the Prelude
  or the benchmarks) keeps the indexer; the scrutinee of a case keeps the
  Variable, so a step with a case still builds one (tak_COMPLEXCASE0,
  isFib); an argument that is both passed on and scrutinized is a Variable
  for both uses; an IVarAccess directly in an argument is read plain, but
  no program of the suite produces one.
- Flat partial applications on the C++ backend (T7).  A partial
  application stored its arguments as a cons list: PartApplicNode held the
  missing count, the head, and a terms list; create_partial built one cons
  per argument, apply allocated a new PartApplic and a cons per added
  argument, and from_partial walked the list.  Done: the arguments are
  inline.  Slot 0 holds the missing count, slot 1 the info table of the
  head, and the slots after them the arguments in call order, so a partial
  application of n arguments has arity 2 + n and the format "ix" followed
  by n times 'p'.  Each n has an info table of its own: PartApplic_Info is
  the table without arguments, and partapplic_info(n) (PartialInfoFamily in
  builtins.hpp, constant-initialized, the first 32 tables by index and the
  rest by a map) makes the others on demand, with the name, flags, and type
  of the base; PartialS has a family of its own (partials_info).  So the
  collector, the copier, the equality, show, the walkers, and the bindings
  read a partial application by its format like any node.  create_partial
  is one allocation, from_partial and rewrite_from_partial copy the
  argument words, apply on an incomplete application allocates one node
  with one more argument (Node::extend_partial), applyS likewise, and set
  copies the arguments under the other family.  A partial application
  without arguments (a function used as a value, f in map f xs) is a node
  of the module, made once at load in the arena of the literal nodes
  (partial_node; internPartialNode in backends/cxx/compiler.py, symbol
  kind PARTIAL_NODE, code P; the missing count is spelled, because the
  info table of a head of the same module is not yet initialized when the
  module's static objects are made); a step that spells one allocates
  nothing, and the Prelude spells 303 of them.  FORMAT_VERSION 7 -> 8.
  Found on the way: the equality compared an 'x' slot (the head of a
  partial application, the set of a guard) as a node, so two partial
  applications compared through the bindings dumped core; it compares the
  pointers now.  The repr form writes the name of the head in place of its
  address (<_PartApplic 1 plus3 <Int 1>>); the str form is unchanged.  The
  binding single_step refuses a root that is not a function application (a
  value has no step; it called a null pointer).  The binding forward_to
  refuses a node of the arena.  Measured with the harness (C++ backend,
  three repetitions, CPU seconds, before to after, ratio; steps, forks, and
  peak RSS equal): Tak1 1.71 to 1.70 (1.00), Fib 0.57 to 0.59 (1.04),
  Reverse 0.66 to 0.66 (0.99), Queens10 3.08 to 2.88 (0.94), PermSort 7.45
  to 7.59 (1.02), QueensSet 1.07 to 1.05 (0.98), Primes 4.08 to 3.31
  (0.81).  Two earlier runs of the same build, under a load of two to
  three from another process, put PermSort at 8.58 and 7.87 and Queens10
  at 2.98 and 3.00; PermSort and QueensSet execute no partial application
  and allocate the same nodes.  Nodes allocated per step: Queens10 1.364 to
  1.044 (-23 percent),
  Primes 1.565 to 1.099 (-30), the others unchanged; Fib never evaluates
  its + thunks (take and isFib walk the list), so no partial application
  occurs in its steps, and the plan's target for it cannot be met by this
  item.  Instructions retired (perf stat, the old and the new runtime built
  one after the other, equal values and steps): Queens10 26.26 G to 25.51 G
  (-2.8 percent; cycles -3 to -6), Primes 29.68 G to 27.97 G (-5.8; cycles
  -16 to -17), PermSort 21.92 G and Tak1 17.87 G unchanged.  The
  acceptance (Fib, Queens10, Primes 15 percent) is met for Primes only:
  the allocation of a node costs about 45 instructions and a cache miss,
  and Queens10 spends its time in hnf, the indexer, and the scheduler, not
  in the 17.5 million nodes this item saves it.  Tests: unit_cxx_partial.py
  with data/curry/CxxPartial.curry (the values on both backends: partial
  applications of no, one, two, and 32 to 35 arguments completed by apply,
  a constructor and a section as values, a function value applied twice,
  set functions over them; the str forms and the equality on both
  backends; on the C++ backend the slots and the info tables, apply
  allocating one node and completing in place or in a new node, the repr
  form, the copies, the shared node of a function value, a loop that
  allocates nothing for it, the refusals of forward_to and single_step,
  the values after many collections, the generated code) and
  data/cxx/partial_test.cpp (the family, create_partial, extend_partial,
  from_partial, rewrite_from_partial, materialize, the copier, the
  equality, partial_node, the collector through the argument slots).
  Left: the allocation of a partial application with arguments remains
  one node per apply (apply (apply f a) b allocates the one-argument
  application); a saturating apply of several arguments is a compiler
  change.  The dictionary of a class method is rebuilt on every use
  (_inst#Eq#Int () allocates the dictionary node; its method values are
  shared now), which an optimizer pass on dictionary passing would remove.
  A partial application without arguments made by the runtime or from
  Python (Node::create_partial, curry.expr) is a heap node.  The repr
  form of the C++ backend differs from the Python backend's, which shows
  the head node (<_PartApplic 1 <plus3 <Int 1>>>).
- Weak free-variable table and finalizers (G3).  The free-variable table
  (RuntimeState::vtable) was a root of the collector and never shrank, so
  every free variable and its generator stayed alive for the life of the
  runtime state, and a lookup by id from another runtime state (a nested
  evaluation started from a Python callback, or a value with a free variable
  evaluated again) found nothing and crashed (status 139).  Done: the table
  lives in the InterpreterState, the owner of the ids, which registers
  itself with the collector; and it is weak: the mark phase pushes no entry
  of it, and after the mark an entry whose node is unmarked is dropped
  (sweep_freevar_tables in gc/wdgc.cpp).  A live configuration keeps the
  variables it names by id, pushed with the queue that holds it
  (push_queue_roots): its residuals (ready asks for them), the keys of its
  bindings (fork applies a binding through the generator of the variable),
  and the ids in its strict constraints (UnionFind::united records every id
  passed to unite; the root of a group stands for the group and receives the
  generator of a narrowed member at the fork).  Every other lookup starts
  from a node in the graph.  A direct-mapped cache of the ids pushed in the
  collection keeps the cost of the binding keys low (the configurations of
  one queue name the same few variables).  get_freevar returns nullptr for a
  missing entry, get_generator throws std::logic_error, and fork and ready
  tolerate it.  The copiers share a free variable instead of copying it
  (copy_node and GraphCopier in graph/copy.cpp), so one node stands for one
  id: a copy with the same id narrowed apart from the variable, and a value
  handed to Python held a node the runtime could not find; now (x, x) with x
  free evaluates to 2 and 4 in a new runtime state.  Finalizers: a generator
  node (a Python iterator as a Curry list) owns one reference to its
  iterator; Node.create takes it (the Arg only borrows the object, the FIXME
  leak is gone), a copy of the node takes one of its own, the step hands it
  to the node of the rest or releases it when the iterator is exhausted,
  and the sweep releases it for a node that dies unstepped, after the sweep
  (release_pending), because a release may run Python code.  Audit of the
  other foreign slots: a _biString node points into the static strings of a
  module, or into the one message buffer of intern_message; the queue and
  the set of a SetEval node go through the registries; nothing else owns a
  resource.  sprite-exec --stats and curry.stats gain gc_seconds, the
  seconds spent in collections, after compile.  Measured on the C++
  backend: a program that narrows a million variables in sequence
  (CxxFreeVars.narrowMany) runs under a 1 GiB cap with 107 MB peak RSS, 45
  collections, and leaves no node and no table entry behind, the same as a
  run of a hundred thousand; Last takes 0.59 s of CPU against 0.73 s (0.80)
  with a peak RSS of 104 MB against 133 MB (0.78); the other nine programs
  of the measured set (QueensSet9, QueensSet, PermSort, Tak1, Fib,
  Queens10, Primes, ColormapFree, PaliFunPats) are within the noise band in
  CPU and peak RSS, with equal counters.  QueensSet9's collector time did
  not move (1.45 s before, 1.47 s after, median of three): that program
  makes 43 free variables in all, so the table was not what its collections
  marked; its live set is the configurations of the lazy set functions
  consumed only in part (120335 configurations in 10212 queues at the
  largest collection, 391384 nodes marked, 12.5 percent of the heap;
  mark 0.165 s, sweep 0.034 s, registries 0.085 s, GC_REPORT build), and
  the cost per collection is the roots of those configurations and the
  destruction of the dead queues.  Tests: unit_cxx_freevars.py (a million
  narrowings under a 1 GiB cap, the table between collections, the shared
  variable of a value and its evaluation, stress mode against plain mode on
  narrowing, binding, unification, a group whose root left the expression,
  a configuration that wakes on a variable another one narrowed, and a
  functional pattern; the finalizer of the generator nodes), unit_cxx_gc.py
  and unit_api.py (gc_seconds).  Left: a free variable built from Python
  (curry.free(), Node.create of _Free) is not in the table and has id 0
  unless given one, so curry.free() inside a goal still gives wrong values;
  intern_message keeps one message, so a second error overwrites the text
  of an earlier _biString node; the Python backend keeps its strong table;
  the dense vector of UnionFind is copied whole on the first unite of a
  configuration, which makes a loop that unites two fresh variables per
  step quadratic (uniteMany); the __deepcopy__ binding takes no memo
  argument.  QueensSet9 needs cheaper configurations, not a smaller table:
  a flat map for the bindings, pooled parts of a configuration, and a
  cheaper destruction of a dead queue (G1, G4).
- Block heap for the C++ runtime (G4, first milestone: the heap with the
  existing mark phase; the conservative C stack scan is not done).  Every
  node came from malloc or a free list through an out-of-line node_reserve
  that pushed a 16-byte entry onto a global vector, and the sweep walked
  that vector and touched every node for its mark bit in the info pointer.
  Done: gc/blockheap.cpp.  Chunks of 1 MB from aligned_alloc are cut into
  64 KB blocks; a block holds slots of one size class (16 to 512 bytes in
  steps of 8) after a header with two side bitmaps, alloc (the slots that
  hold a node) and mark; the block of a node is its address masked, the
  slot a reciprocal multiplication; a larger node gets a span of its own
  with the same header.  node_reserve is an inline fast path in
  graph/memory.hpp (the run of free slots of the size class: a load, a
  compare, an add, a store; the run array is a global of the runtime,
  which a generated module reaches through one indirection), node_refill
  finds the next run of zero alloc bits in the current block (a lazy
  sweep), or takes a block from the class list, the pool of empty blocks,
  or a new chunk; node_commit is inline true.  Counts of nodes and
  allocations are kept per run.  The sweep visits blocks, not nodes: the
  population count of the marks gives the survivors, alloc takes the marks,
  an empty block goes to the pool, a dead span back to the system.  The
  generator nodes are registered at creation (gc_register_generator), so
  the finalizer sweep walks that list.  Nested evaluations: every block
  records the time it left the pool (seq); gc_enter_evaluation records the
  counter in O(1); at a nested collection every allocated slot of an older
  block is a root, the block is not swept and not listed until the
  outermost collection (a superset of the old floor rule, bounded by the
  free room of the heap at the enter).  One ThreadHeap holds the runs, the
  class lists, the pool, and the block list; a worker thread of the
  parallel prototype gets one of its own.  The verifier (verify_heap) runs
  after the mark phase in stress mode and on request (gc_verify in the
  bindings): every marked node is a slot start of a known block (a sorted
  table of the chunks and spans), its alloc bit is set, its info table is
  sane, its alloc_size fits the slot, and every successor is null, a
  literal, the static object of a pinned info table, or a marked slot
  start; a request checks the bitmaps of every block as well.  The
  instrumented build (COUNTERS=1) keeps the creator of a node in a table
  beside its block, written by node_refill: that runtime never publishes a
  run, so every allocation goes through it, and generated code is the same
  in both builds (node_creator is a function now; literal nodes have no
  prefix word any more).  The pushed-id cache of the free-variable roots is
  stamped per collection instead of cleared (an 8 KB fill per collection
  showed in stress mode), and retire_runs visits the classes with a block
  only.  Bindings: gc_verify, gc_block_count, gc_heap_bytes.  Measured
  with the harness (C++ backend, three repetitions, CPU seconds, before to
  after, ratio; steps and forks equal): Tak1 1.71 to 1.29 (0.75), Fib 0.59
  to 0.34 (0.58), Reverse 0.66 to 0.58 (0.88), Queens10 2.91 to 1.77
  (0.61), PermSort 7.58 to 5.20 (0.69), QueensSet 1.05 to 0.76 (0.73),
  QueensSet9 7.34 to 5.15 (0.70), Primes 3.30 to 2.24 (0.68).  Peak RSS
  (memory suite, collector on / off, in MiB as compare prints it): Tak1 105
  to 65 / 1710 to 962, Fib 195 to 115 / 232 to 131, Reverse 83 to 60 / 547
  to 232, PermSort 466 to 312 / 1710 to 958, QueensSet 139 to 105 / 325 to
  231, QueensSet9 395 to 321 / 1758 to 1228.  Nodes per step unchanged (Tak1
  0.9333, Queens10
  1.0437), values equal.  perf stat on Tak1: 16.18 G instructions at 3.28
  per cycle, 8.8 M cache misses (17.87 G, 2.71, 15.4 M after T6).  Stress
  mode: a collection on a heap of 5 live nodes costs 0.42 us (0.07 to 0.12
  before), on 1704 live nodes 25 us, most of it the verifier; unit_cxx_gc
  in stress mode takes 43 s (29 before), unit_cxx_freevars 68 s (37).
  Tests: unit_cxx_heap.py with data/curry/CxxHeap.curry (a constructor of
  70 fields, so its node is a span) and data/cxx/heap_test.cpp (consecutive
  slots of one run, exact counts, the lazy sweep reusing the freed slots in
  address order, a bounded heap over cycles of short-lived nodes, a span
  kept and given back, the rule for nested evaluations through
  EvaluationScope, the generator finalizer with the hooks registered, and
  the verifier finding a successor in a freed slot); the exact counts, the
  bounded heap of a walk, spans through collections, the verifier after
  walks, set functions, and a callback evaluation and in stress mode, the
  refusal inside an evaluation, and the step of tak calling node_refill and
  neither node_reserve nor node_commit (objdump).  unit_cxx_gc.py: the
  collector-off child of the 1 GiB test walks three million cells (a
  million fit under the cap in the dense heap); unit_cxx_counters.py: the
  expected fields include gc_seconds (the instrumented build was not run
  since G3 added it).  Docs: the environment variables, tests/README, the
  gc README.  Left: the conservative C stack scan (stage B of the memory
  note), so a collection in a Python callback still treats every older
  node as a root, and in a coarser way than before (the free slots of an
  older block filled after the enter are roots too); the W7 clause of the
  memory note, QueensSet9 with the collector on not slower than off, is
  open: the entry above compares collector on before and after, and on
  against off the collector costs 1.51 times the CPU (5.23 s against 3.46
  s, memory suite, record phase2-2026-10-04) with 1.29 s in collections,
  25 percent of the run, above the 15 percent trigger of W9 (cheaper
  configurations, G1 and G4 follow-up); memory is never
  given back to the system except the spans of large nodes (a chunk whose
  16 blocks are all empty could be freed); the heap is one global object
  reached by a global run array, not yet per thread; the stress mode is
  slower by the verifier; blocks are 64 KB and the largest small slot 512
  bytes, neither measured against alternatives.
- MPS spike for Gate G (G5).  The 2023 draft of a Memory Pool System back
  end (gc/mps.cpp) did not build, reused the runtime's Fwd info as the
  collector's forwarding marker (unsound), had no valid cold end for its
  stack root, no roots beyond the queues, and generations of 150 KB.  Done:
  make GC=mps builds the runtime with the Memory Pool System of the
  submodule extern/mps (release 1.118, one C object, the hot variety) in
  place of gc/blockheap.cpp and gc/wdgc.cpp; the default stays wdgc.  The
  choice reaches the installation as sysconfig/cxx_gc, the toolchain
  compiles generated modules with -DSPRITE_GC_MPS, and the ABI stamp
  includes the collector, so a switch recompiles every module once (touch
  Make.config, as for COUNTERS; the sysconfig values depend on Make.config
  now).  The back end: one AMC pool of two generations (8 MB and 64 MB by
  default, SPRITE_GC_MPS_NURSERY_KB and SPRITE_GC_MPS_GEN1_KB), an object
  format that reads the size of a node from its info table, forwarding and
  padding objects of the collector's own (GcFwd, GcFwdSz, GcPad, GcPadSz),
  and five roots: an exact root over every registered queue (the root, the
  bindings, and the error of each configuration), an ambiguous root over
  the scan cursors (interior pointers pin the spine), an ambiguous root
  over the nodes Python holds (they never move), an exact root over the
  free-variable tables, and the thread root with the top of the stack from
  pthread_getattr_np as the cold end (the frames of a suspended step and of
  an enclosing evaluation are found there, so a nested collection needs no
  rule about older nodes).  The in-place writers of node.hxx (rewrite,
  forward_or_copy) and rewrite_from_partial pad the slack of a larger block
  with a pad object, because the format walks a segment object by object.
  node_refill is the only allocation path (the runs stay empty); it commits
  the object before the caller writes it and writes a pad object into it
  first, so the heap is consistent at every moment (create_flat allocated
  its fresh variables inside the window and makes them first now).  The
  address-keyed algorithms (the graph copier, the equality, show, the
  unique visitor) hold a GcClamp, which clamps the arena.  MPS schedules
  its collections itself, incrementally, behind protection barriers; the
  safepoint of the scheduler pumps its messages (finalization of generator
  and SetEval nodes, which every creator registers now:
  gc_register_seteval; a queue is destroyed when the last SetEval node that
  refers to it has died) and runs a full collection in stress mode, from
  Python, at SPRITE_GC_THRESHOLD allocations when set, and at the
  configuration threshold.  A SIGSEGV handler installed on top of the MPS
  handler counts the barrier faults and the time in them (gc_fault_count,
  gc_backend_stats, gc_backend in the bindings).  Found on the way: MPS
  scans every root at the flip and admits the ambiguous and the exact rank
  there, so a root of the weak rank fails an assertion; the free-variable
  table is a strong root under MPS, as before G3.  The sets of set
  functions are not freed under MPS.  Measured with the harness (C++
  backend, three repetitions, CPU seconds, the block heap of G4 to MPS,
  ratio; steps and forks equal; each program in a fresh process): Tak1 1.30
  to 2.85 (2.19), Queens10 1.74 to 2.72 (1.57), PermSort 5.13 to 14.21
  (2.77), QueensSet9 5.10 to 19.41 (3.80), Primes 2.27 to 4.16 (1.83).
  Peak RSS with the collector on: Tak1 65 to 44 MB, Queens10 60 to 69,
  PermSort 312 to 830, QueensSet9 321 to 538, Primes 62 to 116.  The
  collector's own statistics over one evaluation through the API, each in
  a fresh process: Tak1 112 collections, 10 barrier faults, 0.91 s in the
  allocation slow path (where MPS traces); Queens10 171 collections, 11
  faults, 0.73 s; PermSort 106 collections, 8995 faults (0.07 s in the
  handler), 9.6 s in the slow path, 722 MB committed, 427 MB of it spare;
  QueensSet9 67 collections and one full one, 11810 faults (0.11 s), 9.0 s
  in the slow path, 52413 clamps; Primes 212 collections, no fault, 1.6 s.
  perf on Tak1: 44 percent of the samples in the kernel (the protection and
  page traffic of the arena), 37 percent in the runtime library (MPS
  included), 14 percent in the module; the nursery size moves nothing
  (Tak1 2.85, 2.87, 2.83 s with 8, 32, 64 MB; Queens10 2.72, 3.20, 3.77).
  A probe that ran the five programs in one process put Primes at 55 s:
  the dead data of the earlier programs in the older generations is
  rescanned by every nursery collection until a full collection comes.
  Signals: with Python's faulthandler enabled at startup the MPS handler is
  installed after it (at the first allocation) and takes the faults;
  nothing is printed.  faulthandler.enable() after the first allocation
  puts Python's handler in front; the test child of that case took no
  fault, so the case is not exercised.  Near an address-space cap MPS
  collects instead of failing: the out-of-memory child of
  unit_cxx_runtime.py reaches 905 MB of RSS under a 1 GiB cap and does not
  end in 60 s, where the block heap raises MemoryError at once.  Decision
  (Gate G): MPS is not adopted.  The rule asked for 20 percent less CPU on
  Tak1 and Queens10 and no loss on PermSort and QueensSet9; MPS costs 1.6
  to 3.8 times the CPU on all five and 1.7 to 2.7 times the memory on the
  search programs (QueensSet9 1.67, PermSort 2.66).  The block heap stays
  the collector; the MPS back
  end stays in the tree behind make GC=mps as the record of the spike.
  Tests: unit_cxx_mps.py (the selection of the collector and the toolchain
  flags on every build; under MPS: the values of programs that collect many
  times, a node Python holds keeps its address, an explicit collection
  reclaims a dropped value, the finalizer of the generator nodes, the queue
  of a set function freed, a nested evaluation, the threshold, the fault
  counter and the statistics, the stress mode, faulthandler at startup and
  after the first allocation); the tests of the block heap, of its
  threshold policy, of the exact counts of queues and sets, of the weak
  free-variable table, and of the out-of-memory child skip under MPS
  (cytest.skipUnlessGcBackend).  The unit suite passes on both builds.
  Left: the inline allocation fast path of memory.hpp is not wired to the
  MPS allocation point (every allocation is a call); the weak table, the
  freeing of sets, and the scheduler counters are not supported under MPS;
  gc_verify reports nothing there; the enable-after-allocation case of
  faulthandler needs a child that provokes a barrier fault.
- Process-split experiment for Gate Q1 (Q1, issue #29).  The
  parallel-evaluation gate asks whether independent parts of a search, run
  on separate workers, could pay: the speedup bound of a split by hand, and
  the work the parts duplicate.  Done, with no runtime change: three
  modules under tests/data/curry/benchmarks/split spell SearchQueens,
  PermSort, and QueensSet whole and split into 2, 4, and 8 parts (the goals
  main and part2_0 .. part8_7).  PermSortSplit fixes the first one, two, or
  three binary choices of the search as the program makes them (the
  insertions of 3, 4, and 5 into the growing permutation; insert forks
  first at the back of the input): it spells the configuration after those
  forks by hand and inserts the other elements on top as perm does.
  SearchQueensSplit fixes the position of the first element: u is a list
  of i free variables in place of the narrowed free list (the branch (_:_)
  i times, then []); the eight positions go to the parts by position modulo
  the count, joined by ?.  QueensSetSplit fixes the first element of the
  permutation (the branch of ndinsert that puts j in front after 1 .. j-1
  went behind: j : perm of the rest); the eight first elements go to the
  parts likewise.  The parts of a split partition the permutations.  Every
  function takes the size, so unit_split.py checks the partition at size 5
  (120 permutations) and the solutions at size 6 (5 on the Python backend)
  against the original modules.  The whole of each split module takes the
  steps of the original (SearchQueensSplit 41373250, QueensSetSplit
  2152979, PermSortSplit 12531155 against 12531154).  The harness got a
  split suite (-s split: the whole as in the throughput suite, and every
  part as sprite-exec -t --stats -m PROGRAMSplit -g partK_I in a process of
  its own; the variants are whole and K/I; three repetitions by default)
  and a split command (run_benchmarks split FILE: per program and part
  count the speedup bound, the whole over the longest part, and the
  duplicated work, the sum of the parts over the whole, by CPU seconds and
  by steps and forks; --parts lists the parts; -m picks wall or eval_wall);
  the counters command names an item with a variant program:variant.
  Measured on the C++ backend (plain build, three repetitions, CPU seconds
  of the whole process, which includes about 0.16 s for the start of the
  process; steps exact; the machine had a load of 1.6 to 4 from another
  process).  SearchQueens, whole 4.59 s, 41.37 M steps, 1.08 M forks: two
  parts 3.25 and 3.02 s, bound 1.41 (by steps 1.62), duplicated work 1.22
  by steps, 1.37 by CPU, 1.26 by forks; four parts 1.80, 1.60, 1.45, and
  1.36 s, bound 2.55 (steps 3.15), duplication 1.22 by steps; eight parts
  0.69 to 0.83 s and one disturbed sample of 1.26 s, bound 3.64 (steps
  5.94, eval_wall 4.81), duplication 1.22 by steps.  The 22 percent are
  the same at every count: a part evaluates permute xs under its own u,
  which the whole shares between the alternatives of the narrowing.
  PermSort, whole 4.71 s, 12.53 M steps, 1.59 M forks: two parts 2.68 and
  2.05 s (6.96 and 5.57 M steps), bound 1.76 (steps 1.80), duplication 1.00
  by steps and by forks; four parts 5.11, 1.85, 3.71, and 1.85 M steps,
  bound 2.41 (steps 2.45), duplication 1.00; eight parts 4.49, 0.62, 1.24,
  0.62, 3.10, 0.62, 1.24, and 0.62 M steps, bound 2.61 (steps 2.79),
  duplication 1.00: nothing is duplicated, but the static split is
  unbalanced, because the sub-tree in which 1 stays in front, the side of
  the solution, holds most of the work.  QueensSet, whole 0.80 s, 2.15 M
  steps, 0.51 M forks: two parts 1.080 and 1.078 M steps, bound 1.77
  (steps 1.99, eval_wall 2.34); four parts 0.51 to 0.57 M steps, bound 2.12
  (steps 3.81, eval_wall 4.28); eight parts 0.24 to 0.30 M steps, bound
  2.98 (steps 7.20, eval_wall 9.56); duplication 1.00 by steps at every
  count.  The CPU bound of QueensSet is the start of the process: a part
  of eight evaluates in 0.05 to 0.06 s.  The cost per step falls in a part
  (a smaller heap, fewer collections): the evaluation times of the parts
  sum to 0.84 to 0.97 of the whole's for PermSort and 0.74 to 0.85 for
  QueensSet.  The scheduler counters of the 30 programs on this tree (make
  COUNTERS=1, one repetition; the records are in the measure directory of
  the build tree): the 16 deterministic programs have a serial fraction of
  1.000 and one configuration; the 12 search programs have 0.000 (RegExp
  0.065, Half 0.002), a queue of up to 94135 configurations (PermSortPeano;
  PermSort 94096, SearchQueens 85637, ColormapFree 27638, ColormapChoice
  15208; Last 6, RegExp 8), median lifetimes of 0 to 8 steps (means 2.0 to
  32.5), shared fractions from 0.163 (PermSortPeano) and 0.194
  (SearchQueens) to 1.000 (ColormapChoice, ColormapFree), and failed
  fractions from 0.002 (Half) to 0.654 (PermSortPeano); QueensSet runs
  0.966 of its steps inside set functions.  PokerChoice and PokerFree do
  not compile (currypp), as before.  The parts carry the fractions of
  their whole (PermSort: 0.492 shared in every part): the shared counter
  measures the sharing between the configurations inside a part, which
  the split does not duplicate; what a split duplicates is the work above
  its point.  The counters build costs 0.94 to 2.11 times the CPU of the
  plain build on the 45 split items (40 slower, 5 the same; every
  allocation goes through node_refill there), with equal steps and forks
  on every item.  Decision (Gate Q1): go.  The rule asked for five
  programs with a serial fraction below 0.5, or a split bound above 2 at
  four parts for two programs; twelve programs have a serial fraction
  below 0.07, and the bound at four parts is 2.55, 2.41, and 2.12 by CPU
  (3.15, 2.45, and 3.81 by steps) for the three programs, with 0 to 22
  percent duplicated work.  The go rests on the split bound, by steps
  first: the CPU of a part includes the start of its process.  The clause
  of the parallel-evaluation note that asks for a median configuration
  lifetime that supports batching is not met (the medians are 0 to 8
  steps), so Q2 and Q3 are judged by a design in which a worker owns a
  subtree, with a dynamic split.  E6 continues with the independent-world
  design (Q2, Q3), on the terms the numbers set: a worker must own a
  subtree (a configuration lives a handful of steps, so a hand-over per
  configuration cannot pay); the split must be dynamic, or deeper than the
  first choices (the static eight parts of PermSort range from 0.62 M to
  4.49 M steps); and the bounds assume no cost for the start of a worker,
  the hand-over, or the merge of the values.  Tests: unit_split.py (the
  partition of the permutations and of the solutions at a small size on
  both backends; the goals), unit_benchmarks.py (the split programs and
  variants, the items and their commands, the default repeat, the variant
  in the counters table, the split command on hand-made records with a
  part missing, a part failed, and the whole failed, and the smoke test of
  QueensSet whole and in two parts on the C++ backend).  Docs: tests/README
  (section 9), the benchmark README.  Left: the split is static and by
  hand, for three programs; the parts run one after the other, so the
  bound says nothing about the memory bandwidth of four processes at once;
  the CPU of a part includes the start of its process (use the bound by
  steps, or -m eval_wall, for a short program); the sample of SearchQueens
  8/6 was disturbed by another process (its evaluation 0.91 s against 0.51
  to 0.67 s of the other parts; the bound by steps does not depend on it);
  the counters of the M4 entry were taken on the pre-Phase-2 tree, and the
  shared fractions moved a little with the in-place rewrites (PermSortPeano
  0.10 to 0.163, ColormapChoice 0.99 to 1.000).
- Nightly performance job with history (M5, issue E1).  The harness had no
  history: the baseline files under tests/data/curry/benchmarks/results
  were the only records, and nothing ran the suites on a schedule.  Done:
  the workflow .github/workflows/perf.yml runs on a schedule (05:17 UTC)
  and on request, builds Sprite on an ubuntu-24.04 runner without ccache,
  and runs three suites on the C++ backend through
  .github/scripts/run-perf.sh: the throughput suite on a fixed set of ten
  programs with three repetitions (NIGHTLY in lib/benchmarks/__init__.py:
  Tak1, Fib, Reverse, Primes, Queens10, PermSort, SearchQueens, QueensSet,
  QueensSet9, Last; the new --nightly option of run_benchmarks selects the
  set, with the expression item in the compile suite and every item of
  the import suite), the compile suite on the same programs (three
  repetitions), and the import suite (five).  The records go to the branch
  perf-history of the repository, written with the token of the workflow
  (permissions: contents: write; no secret) by
  .github/scripts/perf-history.sh, which fetches the branch into a
  worktree (or starts it with an orphan worktree), runs the new history
  command of the harness, commits, and pushes, three attempts when the
  branch moved.  run_benchmarks history DIR FILE... appends the records to
  records/SUITE.jsonl (a record file like any other, so compare reads it),
  writes points.json (one compact object per record, derived from the
  record files) and a README, and prints each new record against the
  previous record of its item with the rows and the summary of the compare
  command; the script puts that table in the step summary.  The job fails
  on a failed run only, after the records are kept as an artifact and the
  history is written; a slow run is never a failure.  The page
  docs/source/extra/perf/index.html (perf/index.html of the built
  documentation, through html_extra_path; linked from the new Performance
  page) is plain HTML and JavaScript without a library: one figure per
  suite made of small panels, one per item (ten or more series in one plot
  would exceed the eight hues a categorical palette allows), each a single
  line of the chosen metric over time with the last value at its end, a
  hollow marker where the rewrite steps changed, a cross on the baseline
  where a run failed, a crosshair with a readout (value, date, commit,
  counters, status) on hover and on the arrow keys, a delta against the
  previous run beyond the 10 percent band, and a table view; the metric
  and the range are selectable.  On GitHub Pages the page reads
  points.json from the perf-history branch through
  raw.githubusercontent.com; elsewhere from a file beside itself, or from
  the URL of ?history=.  Measured here (the commands of the job, the C++
  backend, two runs one after the other on this tree, CPU seconds, the
  counters equal on every item): throughput Tak1 1.297 -> 1.270, Fib 0.353
  -> 0.339, Reverse 0.589 -> 0.581, Primes 2.190 -> 2.229, Queens10 1.740
  -> 1.747, PermSort 5.067 -> 4.930, SearchQueens 4.776 -> 4.904, QueensSet
  0.777 -> 0.770, QueensSet9 5.121 -> 5.086, Last 0.441 -> 0.420 (10 same);
  the compile suite 22 items same (cold compiles 0.92 to 1.51 s, warm 0.48
  to 1.01 s, the expression 0.80 s cold and 0.39 s warm); the import suite
  4 same (python 0.025, import 0.10, prelude 0.16, hello 0.16 s).  The
  three suites take 1m30, 1m11, and 3 s here.  Published twice through the
  script to a bare repository made for the purpose: the first run started
  the branch, the second appended 36 records and printed the ratios; the
  page rendered the result under node (3 figures, 36 panels, 36 lines).
  Tests: unit_benchmarks.py (the nightly set and --nightly, the summary of
  compare, the history command: the files, the second run against the
  first, two files in one call, the options, the errors),
  unit_perf_history.py (the page under node on hand-made points and on
  the baseline records: the figures, the panels, the lines, the deltas,
  the changed counters, the failures, the escaping, the range, the data
  URLs, the formats; the scripts: executable, parsable, and the publish
  script end to end against a bare repository: the branch started, the
  second run appended, a bad file changing nothing; the workflow and the
  names shared between its parts; the docs).  Docs: tests/README section
  9, the Performance page of the documentation, the README of the results
  directory.  Left: a schedule runs on the default branch alone, so the
  job runs once the workflow is there (a dispatch runs on any branch); the
  page needs GitHub Pages enabled (the docs deploy) and a public
  repository for the raw fetch; the page was rendered under node and not
  looked at in a browser; the retry of a rejected push has no test; the
  branch grows by one commit and one copy of the record files per night (a
  squash keeps the files); the memory suite is not in the job.
- ICurry interpreter prototype in the C++ runtime (B2, issue #31).  Every
  module of the C++ backend went through g++ before it could run: a warm
  compile of an expression paid 0.23 s of g++, a module compiled from a
  string about 0.7 s, and the Python backend was the only evaluator that
  needed no compiler.  Done: the runtime interprets a function from a
  bytecode.  InfoTable gets a field aux, the data of the step (null for
  generated code and the built-ins).  cyrt/icurry.hpp defines the machine:
  registers (plain Node * for the arguments a step only passes on, the
  rule of backends/cxx/passthrough.py), variables (Variable objects for
  the scrutinees and the bases of paths), and an operand stack on which
  the arguments of a node are collected; 25 opcodes in 32-bit units (the
  loads and the indexer, copies, a fresh free variable, pushes, MAKE,
  MAKE_PARTIAL, MAKE_STRING, the stores, SET_SUCC for a recursive let,
  EXEMPT, RET_REF, RET_NODE, RET_STRING, CASE_CONS with a branch table by
  tag, CASE_LIT with a value table).  cyrt/icurry.cpp is the one step
  function of every interpreted function (icurry_step): it makes the calls
  of the generated code (Node::successor_node, the indexer of Variable,
  rts->hnf, Node::create, create_partial, and the in-place rewrite of the
  redex when the result fits) and returns the same statuses, so compiled
  and interpreted functions run on one scheduler, take the same steps, and
  allocate the same nodes.  backends/cxx/bytecode.py emits the bytecode
  from the ICurry of a function (compile_function), with a disassembler;
  the constants of a function are the info tables and data types it names
  and the values the runtime makes once (the literal nodes, a partial
  application without arguments, the text of a string, the value set of a
  literal case).  backends/cxx/materialize.py attaches the bytecode when
  it materializes a function of a module imported from its ICurry
  (icurry_attach in the bindings); a function of the module named before
  the loader reaches it gets its table on demand.  The flag interpret
  selects the mode: off (the default: g++ as before), new (a module
  without a compiled object is interpreted from its JSON, the plan ends
  there: Json2Cpp.ends_plan, Maker.done; a module with a current object is
  loaded from it), all (every module is interpreted, the Prelude from its
  JSON, the compiled objects unused).  A module whose functions are
  interpreted keeps its tables for the life of the process (Module::clear),
  because the bytecode of another module names them by pointer.  Found on
  the way: the IOError constructors and the show instance of IOError were
  looked up through the symbols of Prelude.so (dlsym), so a catch or an
  I/O error dumped core when the Prelude was interpreted; the lookup goes
  through the module registry now (Module::find_symbol), which serves both
  forms.  Measured on the C++ backend (CPU seconds of the whole process
  from sprite-exec --stats, median of three; the -t evaluation seconds in
  parentheses; steps and forks equal in every mode): compiled, Tak1 1.31
  (1.13), Fib 0.34 (0.17), PermSort 5.09 (4.89), QueensSet 0.80 (0.61),
  Last 0.44 (0.27), ColormapFree 0.30 (0.16); interpret:new (the program
  interpreted, the Prelude compiled): Tak1 1.87 (1.74), Fib 0.31 (0.18),
  PermSort 5.34 (5.21), QueensSet 0.76 (0.62), Last 0.41 (0.28),
  ColormapFree 0.32 (0.18); interpret:all (every module interpreted; the
  process reads the ICurry of the Prelude and emits its bytecode first,
  0.55 s: Hello 0.14 to 0.69 s): Tak1 2.43 (1.74), Fib 0.93 (0.25),
  PermSort 5.83 (5.11), QueensSet 1.39 (0.61), Last 0.99 (0.29),
  ColormapFree 0.90 (0.21).  So the interpreter costs at most 1.5 times
  the compiled evaluation time (Tak1), and at most 2.7 times the compiled
  process (Fib, where the fixed 0.55 s dominate); the acceptance asked for
  at most 4.  The Python backend takes Tak1 282.8, Fib 29.7, PermSort
  278.7, QueensSet 47.5, ColormapFree 16.5 s (Last fails there with status
  135), 18 to 116 times the interpreted process; the acceptance asked for
  8.  A warm compile of an expression to its
  first value (probe.py with the ICurry cache warm) takes 0.019 to 0.020 s
  under interpret:new against 0.23 to 0.25 s with g++; cold 0.43 against
  0.67 s.  The compiled path is unchanged (compare: 5 same, counters
  equal).  perf on Tak1 under interpret:all: icurry_step 32 percent, hnf 9,
  Variable::skip 9, procS 5.5, the Python emitter of the Prelude 15; 29.6 G
  instructions against 16.2 G compiled.  Tests: unit_cxx_interp.py with
  data/curry/CxxInterp.curry (the emitter on hand-built ICurry: the plain
  loads and the indexer, a case with and without every branch, literal
  cases on Int, Char, and Float with the 64-bit words, the literals and
  strings as constants, partial applications and choices, a free variable
  and a recursive let, aliases and paths, an external body, the errors,
  the disassembler, and the opcode table against the runtime; the values
  of 46 goals of the module under interpret:all against the compiled code,
  with every kind of statement and expression, free variables, narrowing,
  set functions, functional patterns, IO, and strings; the loader under
  new, all, and off; the refusals of icurry_attach; one step at the root of
  a raw expression for each shape of result; Peano, Fib, and Last in
  children in every mode with equal output and steps, and in the stress
  mode of the collector).  The unit suite passes with the flag off (59
  files); under interpret:all the functional suites setfunctions, kiel,
  residuation, complete, examples, funpat, readshow, and io pass, and so do
  the unit files of the Prelude, equality, expressions, inspection, IO
  binding, the collector, the stack guard, the scheduler, the heap, the
  free variables, the examples, and the Curry library.  Docs: the flag in
  interpreter/flags.py, tests/README section 4.  Left: the tiered swap
  (compile in the background and replace the step pointer) and a bytecode
  cache for the Prelude (0.55 s per process under interpret:all; the
  Python emitter and the JSON reader cost it); under interpret:new or all,
  the tests that read a generated .cpp or .so, expect a compiled module, or
  time the compile fail by design (unit_cxx_toolchain, unit_make,
  unit_prebuild, unit_optimize, unit_compile, unit_api, unit_benchmarks,
  and the generated-code tests of the unit_cxx_* files) [corrected in the
  entry of 2026-10-06 on issue #82: those files pin interpret:off for
  themselves and pass under every mode; the tests that read a compiled
  object skip under new and all]; a module object
  held across curry.reload keeps its C++ module alive under the same name,
  so a later import in another mode finds it (the tests drop their module
  objects first); a plain variable is read through successor_node as in
  the generated code, so the interpreter follows the compiled form in the
  limits of passthrough.py; the interpreter is not wired to the Python
  backend's debugging aids (no stepper); the Prelude's JSON is read with
  the object hook on every import under interpret:all.
- Verification velocity: the test runner (V1, V2), deterministic benchmark
  columns (M6), the parallel build and ccache (V3), and the developer setup
  (W1).  Development is limited by verification wall time: every test file
  ran in its own process, one at a time, under ulimit -v 6 GB, about 8.5
  min for the unit files on the C++ backend and 9 min on the Python
  backend; builds never passed -j; benchmarks reported load-dependent
  seconds.  Done:
  (1) Test runner, tests/lib/testrunner (python -m testrunner; run_tests
  execs it): one process per file and backend in a session of its own, a
  watchdog that walks /proc every half second, sums the resident sets of
  the file's session and of the descendants of its process (parent links,
  so a child that started a session of its own counts; the members of one
  poll are the roots of the next), records the peak, and kills the session
  over its cap or past --timeout; admission under -j and a memory budget
  (--mem, 60 percent of MemAvailable by default) with backfill, the two
  backends of one file never together, EXCLUSIVE files alone
  (unit_currylib.py); tests/manifest.json (format 1: duration, peak,
  counts, machine size per file and backend; the cap is twice the peak, at
  least 512 MB, 2 GB without a peak; a file without a duration is assumed
  to take the median of the measured ones); --update-manifest (the peak
  only grows, the duration only from a run with a unittest verdict);
  --fast, --changed REV with RULES and a fallback to everything for the
  unknown, --list, --prepare (sprite-make over the shared corpus, one
  directory and process at a time, before the parallel files, because the
  front end, the ICurry and JSON writers, the code generators, and the C++
  compiler write their products in place); SIGINT, SIGTERM, and SIGHUP set
  a flag that the run loop turns into the kill of every session and status
  130; a pattern that matches no file is status 2; -v at width 1 streams
  the output and hands the file the standard input (the editor targets of
  tests/Makefile pass -v).  The committed manifest holds four durations and
  no peaks; DEFAULT_JOBS is 1 until the calibration below.  Tests:
  unit_runner.py (54 tests on small Python children: admission, watchdog,
  timeout, a child in its own session, an orphan, interrupts, manifest,
  rules, report, cli, prepare).  Known: the sum counts a shared page once
  per process (Pss from smaps_rollup costs 14 ms per read against 0.016 ms
  for stat, measured on a 600 MB process, so the runner keeps stat); a
  spike shorter than a poll in a child can be missed; a process forked
  between the scan and the kill by a parent that is already dead escapes.
  (2) Deterministic columns: the harness counts the user-space
  instructions of the child and its descendants with perf stat
  (-e instructions:u) in one repetition after the measured ones, so the
  measured seconds stay free of perf (the repetition is the last sample,
  "perf": true; perf is detected at start; --no-perf skips it); meta holds
  cpu_model, cores, mem_gb and no host name; schema 2, schema 1 loads and
  compares; compare prints the machine of each file, warns when they
  differ, shows the ratio of the instructions, and has --deterministic:
  the instructions decide with a tolerance of 1 percent, steps, forks, and
  collections are listed beyond it, wall and CPU seconds are advisory
  (~cpu, ~wall); --strict now fails on an item without the metric, so a
  deterministic gate on records without instructions fails instead of
  passing with no comparison.  Measured on a loaded machine: two runs of
  Hello differed by 14 percent in CPU seconds and by 0.02 percent in
  instructions; perf adds about 20 ms of CPU per run.  Left: an allocation
  counter from the collector as a further column (needs a runtime change);
  the nightly job records the new columns once perf is on the runner; the
  baseline of 2026-10-03 has no instructions, so a comparison by
  instructions needs a new baseline; the hybrid-processor sum is untested
  on such a machine.
  (3) Parallel build and ccache: configure --jobs N|auto writes JOBS into
  Make.config and the root Makefile turns it into -j (auto is nproc at
  make time; a -j on the command line wins, and so does make JOBS=N);
  every recipe of the root Makefile says $(MAKE), so the job server
  reaches the sub-makes; the order rules python | cyrt, shlibs:: | libs,
  shlib_extra | libcyrt.a, and .NOTPARALLEL in curry/Makefile keep make -j
  safe; configure --with-ccache puts ccache in front of CC and CXX and
  makes tools/cxx a script that runs ccache in front of the post-install
  compiler.  Checked with dry runs, make -p, scratch models, and
  unit_configure.py only; no real parallel build has run; ccache was not
  installed.  DEFAULT_JOBS in configure is '1' until the trial below.
  Known: every make stage re-archives and relinks libcyrt.so because the
  archive members carry full paths (ar -P); harmless.  Left: the
  post-install compiles do not hit the cache until Cpp2So compiles with -c
  and links separately; PrecompiledHeader.flavor should then read the
  compiler behind the wrapper; CI keeps its own wrapper script until the
  option is proven.
  (4) Developer setup: scripts/setup-dev-machine.sh --prefix DIR (apt
  check without sudo, PAKCS with its SHA-256, optional icurry, the
  checkout with install and object-root linked into the prefix, the conda
  environment from conda/dev-environment.yml, configure, make stage, a
  smoke test on both backends, the next steps), docs/source/
  DeveloperSetup.rst, tests/unit_setup_script.py (dry runs with stand-in
  tools).  Review fixes: --jobs auto goes to configure as auto;
  CCACHE_DIR=PREFIX/ccache is part of the clean environment; configure is
  skipped while Make.config exists and PREFIX/configure-args holds the
  same flags, because configure empties install and object-root
  (--reconfigure forces it).  Verified in dry runs and the unit test only;
  the first real run on the new machine must verify the PAKCS build, the
  conda solve, the --icurry step, make stage, and the smoke test, then the
  personal rules file gets the paths of that machine.
  The quiet window (one agent, nothing else on the machine), in order:
    1. Calibrate the manifest: from tests/, in the clean environment,
       ./run_tests -j 1 --backend both --update-manifest (about an hour);
       review git diff tests/manifest.json; run the files reported as
       "killed: memory" again with --update-manifest (their caps follow the
       recorded peaks); commit the manifest.  Then a parallel trial:
       ./run_tests -j auto --backend both --prepare; compare the wall time
       and the failures with the serial run.
    2. The make -j trial: install ccache; re-run configure with the current
       options plus --jobs auto --with-ccache (configure empties install
       and object-root); time make stage from clean with JOBS=1, then with
       JOBS=auto; then a no-op make stage; then a rebuild after a touch of
       src/cyrt/graph/node.hpp and of src/cyrt/state/rts.cpp (libcyrt.so
       must relink); run unit_cxx_toolchain.py, unit_prebuild.py, and
       unit_currylib.py.
    3. make -j12 stage from clean three times; diff -r two of the installed
       trees (minus time stamps); a race shows as a difference or a
       failure.
    4. The ccache trial: ccache -z; make stage from clean; ccache -s; again
       from clean; the runtime objects must hit on the second build.
    5. Flip the defaults: DEFAULT_JOBS = 'auto' in tests/lib/testrunner/
       __init__.py (after step 1) and DEFAULT_JOBS = 'auto' in configure
       (after steps 2 to 4); update the sentences that say the default is
       1 in tests/README section 10, AGENTS.md, and docs/source/
       Installation/ConfiguringSprite.rst; run unit_runner.py and
       unit_configure.py (they read the constants); let CI replace its
       sprite-cxx wrapper with --with-ccache.
    6. Merge checks: merge py314 into the branch; make stage in the clean
       environment; ./run_tests -j auto --backend both --prepare; a
       benchmark smoke run against a fresh baseline with
       ./run_benchmarks compare --deterministic; then merge.

2026-10-04: the C++ compile-time series (examples 20 to 23)
- Four examples model the compile-time algorithms of a C++ compiler over
  one algebraic model of C++ types, the shared modules under examples/cxx:
  template argument deduction and the partial ordering of partial
  specializations (20), overload resolution (21), a bounded check of the
  traits against the standard (22) and member layout with a search for
  the order of the smallest size (23).  Template parameters are free
  variables, search is non-determinism, and set functions collect the
  results.  The motivation is a future integration with the Circle C++
  compiler, whose compile-time metaprogramming can run Python; Sprite
  would supply the search and the unification, Circle the types.  The
  model simplifies C++: no enumerations, unions, wide character types,
  noexcept or ref-qualified functions, non-type template parameters,
  user-defined conversions, templates in overload resolution,
  argument-dependent lookup, member functions, default arguments, class
  layout (a class has no size), base classes, bit-fields, alignas or
  packing; deduction has no function-pointer or derived-to-base
  conversions (the qualification conversion and forwarding references
  were added to deduceCall when example 20 grew to 33 rows); reference
  parameters are references to const, and to functions, only; common_type
  covers the arithmetic types; the data model is LP64.  Each README lists
  what its example leaves out.
- Review of the series, applied: common_type<T, T> is decay_t<T>, not the
  promoted type, and common_type decays its operands; deduce with the
  pattern T const& takes the const off the argument too, so int const[3]
  deduces T = int[3], and the rules of deduceRef no longer overlap;
  std::nullptr_t does not convert to bool in a call, and the tie-breaker
  of [over.ics.rank]/4.1 names pointers and pointers to members only; a
  reference to const of array type, and a reference to a function, bind
  an argument of that type directly, with no decay; sizeof of an array of
  unknown bound has no value; the printer puts a space before a name or a
  member-pointer declarator ("Foo* Foo::*", "char const* payload"); the
  constructors of Choice are Chosen, Tied and NoMatch, so that a module
  may import CxxType and CxxOverload together; the well-formed generator
  of CxxCheck replaced the term generator of CxxType (every type once: 18,
  194, 1245 and 7330 types at depths 0 to 3), which removed the duplicate
  solutions of the inverse questions.
- Generator shape and search cost (C++ backend): the guarded form of the
  generator (build a compound, reject it when a guard on the operand
  fails) made the search t =:= anyType n four times slower at depth 2 and
  25 times slower at depth 3 (33 s instead of 1.4 s over 7330 types) than
  the constructive form (one sub-generator per category, no rejected
  alternative); example 20 on the Python backend went from 39 s to 7 s.  A
  rejected alternative is a live branch of the search until its guard
  fails, and the cost of a search grows faster than the number of its
  branches.  Worth a note in the user guide on the narrowing idiom.
- Defects met by the streams that wrote the examples, not fixed (the
  suspension of (3) on the Python backend was reproduced here): (1, #59) C++
  emitter: a case whose default branch body is a bare literal or a nullary
  constructor (`_ -> False`) is let-lifted by the front end and the
  emitter writes the literal into a cyrt::Variable, which does not
  compile; the shared modules use explicit alternatives or calls in
  default bodies.  (2, #60) Python backend: show of an Int inside a term that
  =:= bound to a free variable does not finish, while arithmetic on it
  works; CxxType.showNat prints the bound of an array by arithmetic.  (3, #61)
  Both backends: a set function applied to a free variable that =:= bound
  under a choice sees the binding of the first alternative in every branch
  (t =:= (Fund Int ? Fund Char) &> sortValues (set1 id t) gives [Fund Int]
  twice); example 21 forces the variable with $## first, which works on
  the C++ backend; on the Python backend $## raises EvaluationSuspended
  and $!! a ValueError inside the backend, so 21 runs on the C++ backend
  only.  (4, #62) Python backend: length of a list of 5040 elements and sortBy
  on 2000 elements raise RecursionError, so a set with more than about
  1000 values cannot be sorted or counted there (example 23 keeps six
  members).  (5, #63) Python backend: a point-free top-level definition (arity
  0) cannot be applied to arguments from Python.  (6) Front end: a where
  clause after a case expression whose alternatives share its indentation
  is not attached to the equation.  (7) Collector stress mode: example 22
  (seven set functions over 1245 types) does not finish in 300 s;
  STRESS_EXCLUDED in unit_examples.py.  (1) and (3) fixed on 2026-10-05
  (the bug batch, below).
- Follow-ups for the series: user-defined conversions and the ranking of
  user-defined conversion sequences; templates in overload resolution
  (deduceCall is the hook) and the preference for a non-template; the
  reference bindings of [over.ics.ref] with value categories; the
  ordering of function templates and non-deduced contexts in deduction; concepts, with constraint subsumption as a
  partial order over conjunctions of atomic constraints, beside the
  partial ordering of specializations; a Struct type with members, so
  that nested aggregates get a layout, with alignas and packing; more
  traits (remove_extent, remove_all_extents, remove_pointer,
  is_bounded_array, ...) and properties over the partial traits with a
  count of outcomes; a unit test file for the shared library (the scratch
  drivers of the streams checked 101, 52 and 65 assertions); a Circle
  driver: a metaprogram that hands the types of the program under
  compilation to the model and asks the questions of the series; and,
  after the typed builder (#56), the inverse questions of examples 20 and
  21 can take a variable made in Python with curry.free() in place of the
  variables their Curry modules declare.

2026-10-04: typed boundary, stage 1 (epic #48)
- Y1, free variables from Python (issue #50, fixes #33).  curry.free()
  built a raw Free node with the id 0 at every occurrence of the marker,
  and no table of the evaluation knew it; curry.choice used the id 0 as
  well.  Done: through curry.expr a free marker is one shared node per
  marker, a call of Prelude.unknown applied to the Data dictionary of Bool
  (unknown never reads it), built at the first occurrence and cached on the
  marker with a weak reference to the interpreter state.  The step of
  unknown creates the variable with a fresh id of the runtime and forwards
  the node, so one marker is one variable in one expression and across
  expressions; curry.reset installs a new state, and the marker becomes a
  new variable there.  A choice marker is a call of Prelude.?, so its id is
  fresh too.  raw_expr keeps the raw Free and Choice nodes with the ids of
  the markers (unit_expr.py unchanged).  RuntimeState.set_goal registers
  the free variables a goal already holds, on both backends (rts.py creates
  vtable before the call; RuntimeState::register_freevars in
  rts_freevars.cpp): an iterative walk with a visited set that follows
  forward nodes and every pointer successor, so a cyclic goal ends.  The
  walk runs behind a counter of the free variables made outside an
  evaluation, InterpreterState.external_freevars on both backends: the
  builder counts markers and raw Free nodes, single_step counts the
  variables of its discarded state (the let xs free of
  compile(mode='expr')), and make_value counts the free variables it copies
  into a value, so a value of one evaluation placed in a later goal is
  registered.  The item of a generator node (a lazy Python iterator) is
  built after the walk, so the step of _biGenerator walks the item on both
  backends.  Measured on the first pass of the item: the C++ walk costs
  about 0.05 us per node (100 ms for a goal of two million nodes that took
  3 s to build from Python), the Python walk about 0.3 us per node; a goal
  without such variables pays nothing.  The bindings expose
  RuntimeState.vtable (read-only) and InterpreterState.external_freevars.
  Tests: unit_free_markers.py (the goal of #33, two markers, one node per
  marker, the reuse of a marker in a second goal with constrEq and with
  the dictionary of =:=, the goal evaluated twice, a cyclic goal, let xs
  free through compile(mode='expr'), the registration, the counter, the
  variables of a single step, a marker inside a lazy iterator, a value with
  a free variable, a marker after reset, raw_expr, and the choice marker
  with fresh ids).  Docs: Quickstart and UsingTheAPI show unknown
  _inst#Prelude.Data#Prelude.Bool before the first step.  Left: the spelling
  curry.eval(getattr(P, '=:='), x, 1) of the issue is a partial application
  until the typed builder (Y7) supplies the Data dictionary; the payload of
  an IO value is not copied on the Python backend and so not counted; the
  typed builder of Y7 passes the Data dictionary of the resolved type to
  unknown.
- Y2, the signature table (issue #51).  The front end writes the type of
  every function and constructor into M.fint, after the dictionary
  translation, and the translation to ICurry dropped it.  Done: the new
  package curry.typecheck (src/python/typecheck/sigtable.py).  An Interface
  indexes one .fint text by byte offset with one regular expression that
  matches a quoted name first, so that a name such as "(,)" or an operator
  never starts or ends an entry, and records the offset of every Func,
  Type, TypeSyn, TypeNew, Cons and NewCons entry; an entry is decoded with
  the FlatCurry reader (toolchain/flat2icurry/flatcurry.py) the first time
  a symbol asks for it, its end found by a bracket scan that skips quoted
  names.  A Scheme holds the quantified variables (the nested quantifier of
  a class method merged in), the context recovered from the leading
  dictionary parameters (() -> _Dict#C t), the type without them, the
  FlatCurry arity, the number of dictionaries, and the FlatCurry type as
  written.  A constructor's scheme comes from its Type or TypeNew entry.
  The printer writes Curry syntax, Num a => a -> a -> a, with the variables
  named by first occurrence, Apply as juxtaposition, and the names of the
  Prelude and of the symbol's own module unqualified (show_scheme takes
  module= and qualify=).  The instance map comes from the _inst#Class#Type
  names of every loaded module, with the instance context from the type;
  the special constructors stay unqualified in the key ([], (), (,), (->));
  a name of another shape is an error that names curry-frontend 2.0.0, and
  the first read of the Prelude interface checks that
  _inst#Prelude.Num#Prelude.Int exists.  One SignatureTable per
  interpreter, interp.sigtable, memoized per module object (weak keys, so
  the modules of compile(mode='expr') are served while they live);
  Interpreter.reset clears it; the parsed files stay in a process-wide
  cache keyed by path, size and mtime (as interfaces.load_interface does),
  purged of vanished files at reset.  The interface file is searched beside
  the module's .icy under the directory of the source and under the module
  root (Y3 writes that copy), then the front end's own copy under
  <root>/.curry/pakcs-3.4.1/ (A/B.fint for A.B) and under the per-directory
  layout.  Sprite's boxed literals Int, Char and Float get the nullary types
  as their schemes; the other symbols of Sprite's own Prelude (IO, ->,
  _biGenerator, _biString) and the lifted _CASE and _LET functions have no
  scheme.  A missing scheme is None through CurryNodeInfo.scheme and
  CurryNodeInfo.signature and an InterfaceError (a CurryTypeError) only
  through SignatureTable.lookup(symbol, required=True): "no type for M.f:
  no FlatCurry interface found; recompile M or use raw_expr".  expr and
  eval never read the table.  Measured on this item (Python 3.14, one run
  in unit_sigtable.py, logged at INFO): the index of Prelude.fint (410 KB,
  1267 functions, 45 types, 49 constructors) 11 ms; the decode of one entry
  0.1 to 0.4 ms; a cold lookup after reset 0.3 ms; a warm lookup 7 us; a
  full parse of the same file 350 ms.  Tests: unit_sigtable.py (21 tests,
  both backends): the schemes of +, show, fmap, >>=, =:=, ?, unknown, Just,
  [] and _inst#Prelude.Show#[] against the printed form and against a full
  parse; Data.List, Control.SetFunctions and Data.Functor.Identity through
  curry.module; the index against a full parse on Data/Maybe.fint,
  Data/List.fint and the test module tests/data/curry/SigTable.curry (a
  class with a default, two instances, a newtype, two constraints, a
  binding without a signature, a private function); a dynamic module
  compiled with the cache off, then without its interface; the fundamental
  symbols; the instance map; the front-end check; the printer; the reset.
  Left: the start-up check runs at the first read of the Prelude interface,
  not at Interpreter.__new__, so an interpreter that never types anything
  never reads a file; the index costs 11 ms, not the 3 ms of the design's
  probe, because it indexes the types and constructors too and skips every
  quoted name; a dynamic module on a cache hit has no interface until Y3
  lands; Y4 and Y6 take the scheme objects from symbol.scheme or
  interp.sigtable.lookup, the instances from interp.sigtable.instance, and
  the declared arity of a type from interp.sigtable.type_decl.
- Y3, the interface files travel with the ICurry (issue #52).  The table of
  Y2 read the front end's copy of M.fint under .curry/pakcs-3.4.1/, which
  can belong to another version of the source after a hit of the ICurry
  cache (a miss on text v1, a miss on text v2, then a hit on v1).  Done:
  KEY_FORMAT 3 with the columns fint and icurry; the converter copies both
  files beside the .icy on a miss (place_interfaces), the slot writes them
  on a hit, renamed for an anonymous module (rename_module covers the
  Curry-interface header); a missing file is an empty copy and a NULL row,
  so an empty interface file means the front end wrote none; icurry_is_stale
  makes an .icy without them stale, and Plan.is_stale asks the producing
  step too; curry/Make.include stages the library's interfaces beside the
  committed .icy; the icurry route was verified to write both files.  Tests:
  unit_cache.py (miss, hit, hit after a miss on a reverted text, anonymous
  rename, disabled cache, NULL row, old table, second process with zero
  front-end runs), unit_curry2icurry.py (copies, icurry route, staleness,
  installed library).  Left: the overlay archive does not yet carry the
  copies beside the .icy (the review below adds the copies at extraction);
  Y2's fallback to the front end's copy (dropped in the review below).
- Y4, goals: defaulting with the PAKCS table and native dictionaries
  (issue #53, fixes #39; #35 in its refuse-to-save form).  A goal without
  a signature is a function whose leading parameters are class
  dictionaries, so curry.eval(M.main14) with main14 = Just 5 came back as
  a partial application.  Done: curry.typecheck.defaulting applies the
  table of the PAKCS REPL (c2p.pl defaultQualType) to a scheme: Num and
  Integral to Int, Fractional and Floating to Float, Monad and MonadFail to
  IO; Data, Eq, Ord, Read and Show dropped on Int and Float, Enum on Int,
  Monad on IO; a lone Data to Bool; anything else the oracle's sentence
  "Cannot handle arbitrary overloaded top-level expressions" with the
  inferred type and a hint.  Apply (T ..) a is folded up to the declared
  arity of T, so return 5 is IO Int.  curry.typecheck.goals applies the
  goal to one base instance per constraint, in the order of the scheme,
  each the partial application of _inst#Class#Type to no argument
  (interp.expr of the symbol); value parameters left over give a function
  value; an IO goal runs during eval and make_value unwraps it (both
  backends did that already).  One path: goals.make_goal in
  Interpreter.eval (a lone function symbol with dictionary parameters),
  compile(mode='expr') without exprtype (the arity check counts the value
  parameters of the scheme; a text that cannot be defaulted is a
  CompileError), :eval and the new :type of the REPL
  (python -m curry.tools.icy, now installed by make; :load adds the
  directory of the module to the path and both commands import the loaded
  module), sprite-exec -g, and on the Python backend saved modules: save
  refuses a goal the table cannot default and, without a goal, refuses to
  write a program (pass module_main=False for a library); the footer gains
  goalscheme=<FlatCurry type text> and moduleMain builds the goal from it,
  so the saved program runs from any directory.  save also compiles from
  the ICurry-JSON of a module whose loaded ICurry is a bill of materials
  (IExempt bodies): the saved file had a failing stub for every function,
  the known failure of examples/04-static-compile, now removed.  The text
  route lifts a trailing "where x, y free" to parameters of
  compiled_expression, as the REPL does (splitWhereFree), applies the
  goal to fresh free variables, and evaluates the tuple of the value and
  the variables; curry.eval yields Bindings objects that print as PAKCS
  prints them, {xs=[1, 2]} True, with one naming of the unbound variables
  across the parts; with exprtype the clause stays local.  compile.py also
  adds the root of a file module to the search path of an expression that
  imports it.  Tests: unit_goals.py (37 tests, both backends): the table
  on hand-made schemes, the module tests/data/curry/UnsignedGoals.curry
  (main14 .. main33, addOne), text goals, where-free goals, the REPL and
  sprite-exec in child processes, save on the Python backend;
  unit_compile.py testExprType checks values instead of the old errors;
  unit_loadsave.py saves with module_main=False.  Left: curry.expr(M.main14)
  alone still builds the partial application (the typed builder of Y7
  takes it); pure (1, 'a') is an Applicative constraint, which the oracle
  rejects too, so the design's IO unit case is an error case; a lambda
  text goal (\x -> x) fails on the Python backend before this item: the
  expression module is compiled eagerly and importSymbol resolves the
  lifted lambda before loadSymbols loads it (expected failure in
  unit_goals.py); str() of a function value whose head lives in an
  expression module fails in the same way after the module leaves the
  registry; the saved program ignored -g (the metadata of Y11) until the
  fix batch of 2026-10-05 (issue #35: curry.__main__.SavedMain).
- Y5, goal parity and type parity with the PAKCS REPL (issue #54).  The
  functional driver (tests/lib/cytest/testcase/functional.py) skipped
  every goal with a nonzero arity, so a goal without a signature whose
  type has a class constraint, which the front end gives a dictionary
  parameter, was never compared with the oracle: the goals of #39.  Done:
  the driver reads the number of value parameters from the scheme
  (value_arity: the arity less the dictionaries, or the arity when the
  goal has no scheme) and skips only goals with value parameters;
  curry.eval supplies the dictionaries (Y4).  No goal of the existing
  corpus changes status: of the 33 goals with a nonzero arity in the .fint
  files of the ten functional suites, six take dictionaries (kiel) and
  every one has a value parameter too.  cytest.oracle.divine passes the
  goal to the oracle as one argument, so a text with spaces reaches the
  REPL whole; it takes the script to run (script=) and records a failing
  run as a golden that begins with "!error" when asked (record_errors=);
  a run the time limit ended raises as before.  The script
  tests/oracle_type runs the :type command of the REPL, outside safe
  mode, which refuses :type, and prints the type alone.  The suite
  tests/func_goal_defaulting.py (50 tests, both backends):
  data/curry/goal_defaulting/defaulting.curry, sixteen unsigned goals
  (the acceptance list, 1 ? 2, fmap, take 3 [1 ..], two free variables)
  plus goal17 with a value parameter, which the driver skips, and
  rejected.curry (toEnum 65, maxBound + 1) as an intended failure, both
  through the driver; twenty text goals through curry.compile(mode='expr')
  against the :eval of the REPL on the same text, five of them with a
  where-free clause; the three errors (toEnum 65 and maxBound + 1 in the
  table, show [] in the front end) against recorded oracle output, with
  the overloaded type of the message compared; and the :type of the REPL
  against goal.signature for the seventeen module goals and against
  expression_scheme for the twenty-three texts.  Values are compared
  modulo the names of the unbound variables (PAKCS prints a lifted
  variable by its name, {x=x} x and {x=x, y=x} True, Sprite {x=_a} _a and
  {x=_a, y=_a} True), the format of floats (the C++ backend prints
  1.0000000000000000) and the order of the lines; types modulo the names
  of the variables and whitespace (PAKCS prints (a,Char)).  The 79
  goldens are committed; their generation took 90 s on the Python
  backend, about one second per run of PAKCS; a C++ run with the goldens
  in place takes 14 s.  Observed on the oracle: `id` prints as id, a
  function value; an IO goal is refused in safe mode with "Only initial
  expressions of non I/O type are allowed!", which keeps IO goals out of
  the suite (unit_goals.py covers them); :type does not lift a where-free
  clause on either side, so x =:= y where x, y free is a type error on
  both while its evaluation agrees.  Left: Sprite's type comes from the
  .fint, so the type parity shows that the reader and the printer agree
  with the front end, not an independent inference (Y8 adds the engine's
  types); tests/oracle.pakcs expands $goal unquoted, which the REPL joins
  again, but a text with a shell glob character would be expanded;
  func_examples.py and the other functional suites were not re-run here
  (func_smap.py was).
- Review of stage 1 (the findings on Y2 to Y5).  Fixed: scheme_of_function
  collected the dictionaries before one nested quantifier only, so a class
  method with its own constraints (round, truncate, ceiling, floor,
  properFraction) kept its Integral dictionary in the type, RealFrac b =>
  (() -> _Dict#Integral a) -> b -> a, with ndicts 1; the loop now collects
  dictionaries past every quantifier, the context is (RealFrac a, Integral
  b) in the order the symbol takes them, ndicts is 2 while the FlatCurry
  arity stays 1, source_arity is clamped at zero, and unit_sigtable.py
  checks that no function of the Prelude outside the lifted locals
  (f._#lambdaN, f.g.N) keeps a dictionary domain in its type.  make overlay
  now copies M.fint and M.icurry from .curry/pakcs-3.4.1/ beside each
  extracted .icy (target overlay-interfaces, OVERLAY_DIR=): the committed
  archive holds 1187 .icy files and no copies, so after an extraction every
  test module was stale and converted again at its first import, which undid
  the speed-up and replaced icurry's file with the port's;
  unit_curry2icurry.py extracts two modules into a scratch directory and
  runs the target.  tests/oracle.pakcs and oracle.kics2 quote the module and
  the goal, so 1 ? 2 and 2 ^ 3 reach the REPL unexpanded
  (func_goal_defaulting.py runs the script with a one-character file in the
  working directory).  The golden of a text goal records its text on its
  first line, "!goal <text>", and is made again when the text in TEXTS or
  ERRORS changes; before, it followed the time stamp of defaulting.curry
  alone; the 46 text goldens were regenerated with the header.
  curry.eval(M.addOne, 1) built (addOne 1) with the literal in the
  dictionary slot and no word; goals.check_application now raises a
  CurryTypeError that names curry.compile(mode='expr'), exprtype and the
  dictionaries-first form, for the positional and the list form; explicit
  dictionaries (goals.is_dictionary: an _inst# symbol, a node of one, or a
  list headed by one) and raw_expr pass as before.  The comment of pass 2 of
  the table claimed the order of the context does not matter; it does for
  [Enum a, Data a] (Bool) against [Data a, Enum a] (rejected, the order the
  front end writes), now pinned in unit_goals.py. interface_candidates no
  longer lists the front end's copy under .curry/pakcs-3.4.1/: with Y3 every
  .icy has its copies, and the front end's copy can belong to another
  version of the source after a hit. Y3's documentation was applied to
  tests/README, AGENTS.md and this entry.  Left for stage 2 (the items of
  the epic after Y5): the typed builder takes curry.expr of an unsigned goal
  and curry.eval with arguments (today an error), the Data dictionary of
  unknown and the =:= spelling of #33; an independent inference for the type
  parity; the REPL page of the documentation; -g in saved programs; and the
  items the reports left: the start-up check at the first read of the
  Prelude interface, the 11 ms index, the lambda text goal and str() of a
  function value from an expression module on the Python backend, the
  Applicative case, an overlay archive rebuilt with the icurry route that
  packs the copies, and the functional suites that were not re-run.

2026-10-04: integration of the three streams (wt/integrate)
- Merged into one tree, in this order: py314 (the C++ compile-time series),
  wt/phase2, wt/typed, wt/velocity.  Conflicts and their resolution: TODO
  (every entry kept, in date order; the bullets of phase2 and velocity
  continue the entry of the performance program); tests/README, AGENTS.md
  and the docs (every side's paragraphs); rts.hpp (phase2's InterpreterState
  with the weak table, and typed's external_freevars; register_freevars
  declared beside freshvar); rts_freevars.cpp (the registration walk writes
  to the table of the interpreter state, where phase2 moved it); copy.cpp
  (phase2's GcClamp and typed's count of the free variables of a value; a
  free variable is shared, not copied, under phase2, so the count moved to
  the shared branch); string.cpp (the generator step registers the free
  variables of its item and releases an exhausted iterator); graph.cpp (the
  single-step binding counts the ids the step made, and the vtable property
  reads the table of the interpreter state; test_counter of
  unit_free_markers.py asks for the id of the raw node instead of the whole
  table, which the evaluations of one interpreter now share);
  tests/lib/benchmarks (velocity's schema 2 with phase2's split suite;
  compare.summary takes the deterministic flag and serves main and
  history.py; the records of both committed labels are schema 1); the
  history tests of unit_benchmarks.py expect the instr column.
- Fixed after the merges.  test_nothing_selected of unit_runner.py named
  the Python backend alone in its manifest, so under backend:cxx the fast
  tier selected the file and ran it (status 1); the manifest names both
  backends.  test_stress_mode of unit_cxx_interp.py (Last under
  interpret:all and SPRITE_GC_STRESS=1, 120 s) timed out: typed's
  curry.eval kept its Goal object, and with it the Python wrapper of the
  goal node, alive while the evaluation ran.  A root on the original goal
  node keeps the choices pulled to the root, with their failed
  alternatives, reachable: the history of the search, which grows with the
  steps, so every collection of the stress mode marked it.  Last with 1000
  elements under the stress mode took 1.9 s with the wrapper kept and 0.02
  s without, on the phase2 build as on the merged one; Last through
  sprite-exec ran out of the 120 s before the fix and takes 2.5 s after it.
  Goal.values now returns a generator that holds the names of the free
  variables and not the goal, so curry.eval drops the goal object when it
  returns.  Left: a Goal object the caller holds through the evaluation
  (the REPL, the default goal of a saved module, a user who keeps the
  result of curry.compile(mode='expr')) still roots the history of its
  search; a remedy on the runtime side is open.  Not of the merge, seen
  on the Python backend with the typed build as with the merged one:
  the five tests of TestInstantiation in unit_py_runtime.py and
  test_apply_nf of unit_prelude.py fail, because compile(mode='expr')
  with a free variable returns a Goal, which interp.expr and
  cytest.step do not take, and its values are Bindings objects.

2026-10-05: finishing the integration branch
- compile(mode='expr') returns a node again.  After the typed branch a text
  with a where-free clause gave a Goal object whose values were Bindings for
  every lifted variable, which TestInstantiation of unit_py_runtime.py and
  test_apply_nf of unit_prelude.py do not take: they build a free variable
  with `x :: T where x free`.  The rule now: a lifted variable whose type is
  absent from the result type of the expression cannot show in a value, so
  its binding is reported (goals.absent_from_result compares the parameter
  types of the scheme of compiled_expression with its result type); a
  variable whose type occurs in the result type is left in the value, which
  is the answer.  `xs ++ [3] =:= [1,2,3] where xs free` still gives
  {xs=[1, 2]} True; `x where x free` gives _a, `(x, y) where x, y free`
  gives (_a, _b), `head (x:xs) where x, xs free` gives {xs=_b} _a, and
  `x =:= 1 where x free` gives {x=1} True.  The names travel with the node:
  the expression module declares `data SpriteLiftedGoal a = SpriteLiftedGoal
  a`, the root is that constructor around the tuple of the expression and
  the reported variables, and curry.eval finds the names in a registry of
  the interpreter keyed by the info table of the constructor
  (goals.register_lifted and goals.lifted_goal; reset clears it).  A
  constructor node is never rewritten, so the root is recognized after an
  evaluation as before it; a first design keyed the registry by the call of
  compiled_expression, which the first evaluation rewrote in place.  A side
  table keyed by the node was not an option: the Python node has no weak
  reference slot and the C++ wrapper hashes by value.  The Goal objects
  stay inside curry.eval, the REPL and sprite-exec.  The derived Data
  instance of the new data type exposed the ordering defect behind the known
  failure of test_lambda_goal: the Python backend compiled an expression
  module function by function while the module loaded, and a reference to a
  function of the module loaded later (a lifted lambda, a derived method)
  failed in importSymbol.  An expression module now loads with lazy
  trampolines like any module and is compiled right after its load, before
  it leaves the registry (Backend.compile_pending); test_lambda_goal passes
  on both backends.  The lifted variable is a free marker whose first step
  makes the variable, so test_apply_nf steps four times.  PAKCS prints the
  binding of every lifted variable, {x=x} x, so func_goal_defaulting.py
  drops from the golden the bindings of the variables Sprite leaves in the
  value before the comparison (drop_bindings); the goldens are unchanged.
- The Goal object no longer roots the history of a search.  curry.eval
  dropped its Goal after the merges, but the REPL's :eval, sprite-exec and
  a saved module's default goal held the goal node in a local variable
  through the evaluation, and the history (the choices pulled to the root
  with their failed alternatives) stayed reachable from it: under
  SPRITE_GC_STRESS=1 every collection marked it, and the run grew with the
  square of the search.  Now Goal.evaluate hands the node to the runtime
  state and keeps no reference to it (a second evaluation of the same Goal
  is a ValueError; curry.eval makes a new Goal per call), the REPL passes
  the compiled expression straight to curry.eval, and sprite-exec builds
  the goal when the evaluation starts.  Measured through the REPL under the
  stress mode: last (replicate 3000 True) took 26.5 s before and 1.0 s
  after; 8000 elements 1.1 s after; Last through sprite-exec 2.5 s, as the
  merge entry measured with the Goal dropped in curry.eval.
  test_repl_eval_under_stress of unit_goals.py runs the REPL on 8000
  elements under the stress mode within 60 s, on the C++ backend.
- The verify agent's five commits (ced529a2, 12677dfe, 01341ecb, b1b77b17,
  3eae7bcb).  Calibration of tests/manifest.json on a quiet 12-core, 23 GB
  machine, every file once per backend at width 1 with --update-manifest:
  the Python backend 446.8 s serial, the C++ backend 885.6 s serial on a
  cold tree (the durations held the compile time of each corpus,
  func_eqconstr.py 335 s) and 619 s on the warm tree, which the second
  calibration recorded, for 154 files; the parallel trial of the whole
  suite, -j auto --backend both, 123.1 s with 176 files ok and 4 known
  failures.  Test-runner fixes found by the trials: the module list of the
  prepare pass took the product directory .curry for a module and listed
  helloExternal.curry, whose external no backend resolves (the list keeps
  files alone; helloExternal joins badimport in EXCLUDE); the pass compiled
  UseConc1 and UseConc2 of kiel, whose import ModConc lives in kiel/lib, so
  the disk scan of func_flat2icurry.py resolves kiel/lib as well; the pass
  failed on PokerChoice and PokerFree, which need currypp (both join
  EXCLUDE; the benchmark harness compiles them in its warm-up); test_convert
  and test_tool_selection of unit_curry2icurry.py run with the ICurry cache
  off, because a hit never runs the front end whose files they check.
  test_modules of unit_runner.py now pins the three exclusions.  The
  overlay finding: a fresh checkout followed by make overlay failed
  unit_cxx_gc.py, because CxxGc.curry changed after the archive was packed
  (commit 64f715d1) and the toolchain took the extracted .fcy for current:
  filesys.newer compares the change times of the inodes, and an extracted
  file is newer than its source by that measure whatever time the archive
  records (2000-01-01).  Fixed in the overlay rule rather than by a new
  archive, because the .icy files of this tree come from the port and the
  archive is the port's oracle (section 8 of tests/README): the new target
  overlay-prune removes, after the extraction, the products of every test
  source that git shows changed since the commit that last changed the
  archive, in a later commit or in the working tree, in both product
  layouts; without a git history it prunes nothing and says so.  Verified
  on a scratch copy of the pool with the archive's products extracted:
  unit_cxx_gc.py failed 7 tests with 1 error on the old module before the
  prune and passed its 23 tests after it, with the .fcy made again;
  test_overlay_prune of unit_curry2icurry.py picks a changed source from
  git (CxxGc today) and an unchanged one and checks the rule on a scratch
  extraction.  The build trials of issue #43 on the same machine: serial
  from scratch 172.2 s; parallel (-j auto) without ccache 58.6 s, of which
  the prebuild of the Curry library takes 41 s; with a cold ccache 77.3 s;
  with a warm ccache 47.3 and 44.7 s; a no-op build 6.3 s; after a touch of
  node.hpp 7.8 s with ccache and 20.7 s without.  Verdicts: the parallel
  build takes 0.34 of the serial time, a third within noise, bounded by the
  serial prebuild; the two trees are identical except the member order of
  the archive and the name of the precompiled header; the second build with
  ccache is under a minute: met.  The prebuild of the Curry library runs
  serially inside a parallel build and is the next bottleneck (41 of
  58.6 s).
- The defaults flipped after the calibration: DEFAULT_JOBS is 'auto' in
  tests/lib/testrunner/__init__.py (the budget decides the width, up to
  the core count, on the caps of the calibrated manifest; -j 1 runs the
  files one at a time) and in configure (one make job per processor;
  --jobs 1 is a serial build).  tests/README, AGENTS.md, the configuration
  page of the docs, the help texts and the tests that read the constants
  follow.  The next bottleneck of the parallel build is the prebuild of the
  Curry library, which runs serially inside it: 41 of the 58.6 s of a
  parallel build without ccache.

2026-10-05: tiered execution (B3, issue #64)
- Tiered execution on the C++ backend: the interpreter flag ``interpret``
  gets the mode 'tiered', now the default.  A module without a current
  object is interpreted at once, as under 'new' (Json2Cpp.ends_plan;
  Cpp2So.ends_plan ends the plan at a generated .cpp without an object as
  well, and loadcurry reads the JSON beside it, so a compile that was
  cancelled leaves nothing in the path), a child process compiles it in the
  background, and the runtime swaps the functions of the module to the
  compiled code when the object is ready.  The pieces: backends/cxx/
  tiered.py (the jobs, the shims, the policy, poll, wait, status, counts,
  cancel), cyrt/tiered.hpp and tiered.cpp (a detached worker thread that
  runs the commands of a job with posix_spawn, one job at a time in the
  order the modules finished loading, and the swap on the evaluating
  thread), Module::adopt, Module::keep_tables, SharedCurryModule with a
  load path, and the bindings (tiered_submit, tiered_poll, tiered_results,
  tiered_status, tiered_wait, tiered_cancel, tiered_build_shim,
  tiered_load_shim; InfoTable.address, InfoTable.has_step,
  DataType.address, Module.shlib).  The child runs sprite-make --so -z on
  the source file with the search path of the import and interpret:off;
  sprite-make sets interpret:off for the interpreter it makes, so an
  explicit compile compiles under any mode, and so does the prebuild of
  make stage.  The hooks: IBackend.module_loaded (the end of an import
  registers the shim of the module and queues the job),
  IBackend.before_evaluation and IBackend.after_evaluation
  (Interpreter.eval applies the finished jobs before an evaluation, and the
  goal applies and logs them when its values are exhausted); the scheduler
  applies them at its periodic safepoint too (check_interrupts, every 65536
  steps), so a running evaluation swaps in the middle: a step in flight
  finishes on its bytecode, the next step of the function runs compiled.
  Interpreter.stats and the --stats line gained the fields swapped (the
  functions swapped) and failed_compiles.  Policy: an expression module, an
  interactive module, and a module compiled from a string
  (str2module.is_temporary) stay interpreted; a failed compile, shim, or
  load leaves the module interpreted and is logged once per module, by the
  next poll, at the end of the evaluation, or at exit; a reset, a reload,
  and the exit of the process cancel the queue and kill the running child
  (its own process group), so no compiler outlives the interpreter and a
  short program never waits, and a program shorter than its compile ends
  interpreted with no object written; the Prelude loads compiled from the
  prebuilt object as before.
- The swap and the one-table rule.  The runtime compares info tables by
  address (graph/equality.cpp, RuntimeState::add_binding, inspect.isa), so
  the compiled object must use the tables the interpreter made.  A compiled
  object names its tables and types by ELF symbol, its own included
  (R_X86_64_GLOB_DAT in the code, R_X86_64_64 in the bill of materials and
  the constructor arrays), and the dynamic linker binds a reference to the
  first definition in the global scope.  So tiered.py registers a shim per
  module with tables made at run time when its load ends: g++ -shared
  -nostdlib over an empty file with one --defsym per table and type, the
  address of the object made at run time (21 ms), loaded RTLD_GLOBAL before
  any object (a job carries every shim not yet loaded; the loader loads
  them before an object it loads).  The dynamic initializer of a static
  table of the object constructs it at the address its symbol resolves to,
  so the load of the object writes the compiled step, the name, the format,
  and the flags into the tables of the interpreter, and the object's own
  copies stay blank.  The load is the swap; Module::adopt counts the
  functions, takes the step of a function the initializers did not reach
  from the step function's own symbol and clears its aux field, so a
  swapped table looks the same on both paths, clears the static-object flag
  the initializers wrote, and links the object.  Module keeps the tables it
  made in owned lists; a module with tables made at run time, under an
  interpreting mode, moves them to a process-wide kept registry when it is
  cleared, with its object (the kept tables hold its steps), and a module
  of the same name made later takes a kept table of the same tag and arity
  back (create_infotable, create_type), so a shim stays valid across a
  reload; a module loaded from its object has nothing to keep, and its
  object goes with it.  A module whose tables changed shape after its shim
  was made stays interpreted, and so do the modules that import it (their
  objects would bind to the old tables), with one warning.  The review of
  the first version found and this version fixed: (1) dlopen names a loaded
  object by its path, so a file written over the path of a mapped object
  (the kept object of an earlier incarnation) was never read and the swap
  installed the old code; the swap probes with RTLD_NOLOAD and loads such a
  file through a hard link of its own name beside it (a copy when the file
  system refuses the link), removed once the object is mapped;
  SharedCurryModule(sofilename, loadpath) records the canonical name and
  takes over the registry entry of the module.  (2) Once dlopen ran, the
  initializers have written the live tables, so no failure after the load
  unmaps the object: the swap opens the object itself first and never
  closes that handle on a failure path.  (3) A compiled object names the
  objects of its imports as needed libraries, which the dynamic linker
  mapped before the loader processed the imports, stale or not: an edited
  import left a stale object mapped and two tables per symbol.  Cpp2So
  .is_stale reads the imports of a .so from the generated file beside it
  and imports them first; an object is loaded only when every import was
  loaded from its object, else the module is interpreted (or compiled
  again under 'off'), and its compile waits in the queue behind the
  compile of the import.  (4) A result applied at a safepoint was never
  logged in a process that evaluates once: the end of an evaluation and
  the exit of the process now poll, and the stats line carries the counts.
  (5) The shim of a module is registered when its load ends, not computed
  for every module of the interpreter before an object loads, so a
  half-loaded module (in the registry before its symbols) no longer logs
  a spurious shape-change warning and consumes the once-key.  (6) The
  kept-table rule keeps a module only when it has tables made at run time:
  a module loaded from its object is freed with its registry entry as
  under 'off'.  (7) A kept table keeps its step on both swap paths; the
  revert to the interpreter in clear is gone.  Also found: the destructor
  of a std::condition_variable blocks until its waiters left (glibc), so a
  static one with the worker waiting on it hung the exit of every process
  that queued a job; the state of the worker is a leaked singleton.  A job
  popped before a cancel used to start anyway; a job's command starts
  under the lock after a check of the generation.  DataType.flags is a
  char in the bindings.  A re-import of a module whose C++ module is still
  held found swapped tables with a step; the materializer attaches a body
  to a table without a step only (InfoTable.has_step).
- Found on the way to the default.  The runtime headers changed, so every
  object compiled before (the test corpus included) was stale by its ABI
  stamp.  Under 'off' a test process compiled such a module again in place;
  under 'tiered' it interprets the module and the background compile dies
  with the short process, so the tests that inspect the compiled object of
  a corpus module (unit_cxx_bom, unit_cxx_literals, unit_cxx_partial,
  unit_cxx_passthrough, unit_cxx_rewrite) fail until the prepare pass of
  the runner (./run_tests --prepare) compiles the corpus; tests/README and
  AGENTS.md say so.  A test that pins 'off' by a reload must import the
  Prelude before it fakes a step of the toolchain: a new interpreter loads
  the Prelude on first use, the plan of the installed Prelude then ran
  under a mocked format stamp, took the installed .cpp for stale,
  regenerated it from the module whose C++ tables were still the static
  ones of the object (3448 extern declarations, no definition), and the
  next process compiled that stub into the installation; every file that
  loaded the Prelude failed with an undefined symbol until make stage
  rebuilt it.  Two hazards remain and are listed below.  The mixed mode
  had a gap: an interpreted module that applies readFile of the compiled
  Prelude to a nondeterministic name raised ValueError('bad Curry string')
  where compiled code and a program interpreted whole raise the
  nondeterminism error; the entry on the saturation of apply chains (O1)
  found the cause, a primitive that carried the name of its wrapper, and
  closed it.  The tests that drive the compile steps set the flag to 'off' for
  themselves: unit_cxx_toolchain.py (per test, with the Prelude imported
  first), test_values, test_private_target_across_modules,
  test_linked_against_the_target_module, and
  test_target_module_joins_the_imports of unit_optimize.py,
  test_compile_time of unit_api.py, testReusedModuleNameIsAnError of
  unit_compile.py, and test_module_in_a_package of unit_prebuild.py;
  test_split_items of unit_benchmarks.py accepts the other flags of the
  environment.
- Measured on the final build (the machine carried another stream; load
  average 6.8 to 8.6 during the runs; three repetitions each; the flag
  interpret:off against the default).  Time after the front end, in
  process: the import of a fresh module from its JSON with no object, Fib
  0.592 to 0.723 s under off (json2cpp and g++) against 0.006 to 0.010 s
  under the default, Peano 0.769 to 0.816 s against 0.010 to 0.013 s; the
  first value of main then takes 0.302 to 0.307 s compiled against 0.332
  to 0.340 s interpreted for Fib, 0.164 to 0.189 against 0.241 to 0.252
  for Peano (the compile ends after the program).  The expression 1+2 in a
  child Python (the probe of the harness), warm ICurry cache: curry.compile
  0.380 to 0.414 s against 0.024 to 0.030 s, the first value 0.6 to 1.0 ms
  in both.  The compile suite (./run_benchmarks -s compile -b cxx -r 3,
  Fib Peano Last expression, wall seconds of the whole process, off against
  the default, with the ratio of the instructions): warm Fib 1.008 ->
  0.419 (0.44), Peano 1.240 -> 0.348 (0.36), Last 0.870 -> 0.333 (0.48),
  expression 0.672 -> 0.337 (0.60); cold Fib 1.738 -> 1.107 (0.73), Peano
  1.862 -> 1.207 (0.64), Last 1.555 -> 1.169 (0.77), expression 1.446 ->
  1.072 (0.84); 8 of 8 items faster by wall and by instructions, the
  counters equal.  sprite-exec -t --stats on a fresh copy, the front end
  included, wall seconds off against the default: Fib 1.78 to 2.06 against
  1.30 to 1.41 (eval 0.29 to 0.31 against 0.33 to 0.36), Peano 1.92 to
  2.03 against 1.22 to 1.30 (eval 0.17 to 0.20 against 0.25 to 0.33), Tak1
  3.18 to 3.55 against 3.14 to 3.26 (eval 1.80 to 1.83 against 1.86 to
  1.89, swapped=6 in the stats line: the swap during the run); steps equal
  in every pair (3500006, 1857970, 33629915).  The acceptance, under 50 ms
  to the first value after the front end: met in the reading "the import
  and the first step" (6 to 13 ms for a module, 24 to 30 ms for an
  expression to its first value); the first value of a program is its
  evaluation, and the interpreter takes 1.1 to 1.5 times the time of the
  compiled code for it.  The suites: ./run_tests -j 4 --mem 6 --backend
  cxx 'unit_*.py' 66 of 66 files, 0 failed (110 s; 68 of 68 with
  --prepare, 190 s); --backend py unit_compile.py unit_api.py
  unit_py_evaluation.py 3 of 3, 0 failed; unit_cxx_tiered.py and
  unit_cxx_interp.py under SPRITE_GC_STRESS=1: 16 tests (one expected
  failure) and 25 tests, ok.
- Tests: unit_cxx_tiered.py (16 tests, one an expected failure): the swap
  and its counts in tiered.status and curry.stats, the values and steps of
  both tiers, one table per symbol (the same address before and after,
  isa, == across the tiers), the swap between the values of one evaluation
  and at a safepoint of a long loop (a skip when the compile outlasts the
  loop), a fresh module that imports a fresh module, the tables and the
  shim across a reload without a warning, an edited module after a reload
  (the new code runs after the swap), an edited import with a current
  dependent (the dependent is interpreted and both swap), a module
  compiled from a string made again after a reset (the tables taken back,
  the new value), the failure path with one warning and the stats field
  (CXXFLAGS the compiler refuses), strings and expressions left alone, a
  reset that cancels (both outcomes of the race), a compiled module that
  loads compiled, sprite-make under the flag, a fresh program under
  sprite-exec with the output and steps of the compiled run and no object
  left behind, a failed compile reported once on the standard error of
  sprite-exec with failed_compiles=1, and the expected failure of the
  mixed mode.  Docs: flags.py, tests/README section 4, AGENTS.md, the
  sprite-exec page (the two fields).
- CI under the default.  The shards run each file with unittest discover
  through .github/scripts/run-tests.sh on a fresh checkout, so no corpus
  object exists when a run starts: the five files above failed on the C++
  backend, and every other file ran interpreted.  The runner gained
  --prepare-only (cli.prepare_pass_jobs, report.prepare_header): the
  prepare pass alone, for the pool and the corpus of each selected
  functional test, one process at a time, no test file; the exit status is
  0 when every directory compiled whole, else 1 (under --prepare a failed
  directory stays a note), and --list shows the directories.  run-tests.sh
  collects the files of the shard first, runs the pass over them on the
  C++ backend before its loop, in a group of its own, and fails the shard
  when the pass fails; the py shard, and the flags interpret:all and
  interpret:new (a test process never compiles under them), skip it.
  Test: test_prepare_only of unit_runner.py (a stub pass with both
  statuses, no test file runs, --list).  Verified with the objects of the
  corpus removed: the script on unit_cxx_bom.py compiled the pool in the
  pass (23 s) and the file passed, unit_cxx_rewrite.py found the products
  current (0.5 s) and passed, unit_cxx_interp.py under
  SPRITE_TEST_FLAGS=interpret:all ran without the pass and passed, and a
  CXXFLAGS value the compiler refuses failed the pass and the shard before
  the loop with the compiler's message in the group.
- Left: the runner could run the prepare pass by itself on the C++ backend
  when a corpus object is stale, so that the plain ./run_tests needs no
  flag after a header change; a plan with a stale verdict on an installed
  object writes into the installation, and a process that holds the
  compiled module of that name emits a stub (the generation step imports
  the ICurry into the live module); a program shorter than its
  compile never gets an object under the default (a run that let the child
  finish at exit would give the next run compiled code, at the price of a
  compiler that outlives the process); the shim names the first
  incarnation of a module name, so a re-import after a change of shape
  stays interpreted with its dependents (a take-back of a kept table
  regardless of shape would remove the limit); a swapped module keeps its
  registry entry for the life of the process, so a later load of the same
  name from another directory is refused; a module whose compile the exit
  cancelled keeps its generated file and compiles from it next time; the
  latency items are measured by hand here and not yet in the nightly
  history; the Prelude's bytecode cache (B2) is still open.

2026-10-05: the bug batch (issues #32, #34, #36, #38, #59, #61)
- Set functions over a decided argument (#61).  `let t free in (t =:= (A ?
  B)) &> [sortValues (set1 id t)]` gave [[A]] for both alternatives on both
  backends.  Cause: the application of the set function sits below the
  pull-tab of the constraint, so the alternatives share the node; the first
  to reach it ran evalS once and wrote `Values (allValues (SetEval s q))`
  into the shared graph.  The nested queue reads the fingerprint of the
  enclosing configuration through the queue stack (read_fp and fork walk
  qstack, rts_fingerprint.cpp), and choice_escapes (rts_setfunctions.cpp)
  keeps a choice inside the capsule when an enclosing configuration has
  made it, so the capsule pruned the generator of t to the side of the
  first alternative and cached A in the shared allValues node, which the
  second alternative read.  A choice the configuration decided before the
  set function saw it had the same defect, in the argument (`let x = A ? B
  in (x, sortValues (set1 id x))` gave (A, [A]) and (B, [A])) and in the
  function position (set1 (constT x) 0).  Fix: evalS_step
  (currylib/setfunctions.cpp) and evalS (backends/py/currylib/
  setfunctions.py) walk the PartialS node before the rewrite
  (holds_private_state, _holds_private_state): a free variable with a
  binding, a narrowing, or a group, or a choice the configuration decided
  (read_fp, through the queue stack), makes the step private, and the
  replacement goes into a private copy of the spine from the root
  (copy_spine and E_RESTART; rts.restart() on the Python backend), as
  replace_freevar does for a binding; the shared node stays an application
  for the other configurations, which make their own set and queue.  The
  walk follows every pointer successor (calls, forward nodes, guards,
  partial applications, data, the alternatives of an undecided choice),
  does not enter the generator of a free variable (the variable is the
  test: nothing below its generator is decided while it is not), and gives
  up after 64 nodes with the conservative answer, private, which only loses
  sharing.  When the outermost queue holds one configuration and no queue
  encloses it, no other configuration reads the shared graph and a later
  clone starts with the same state, so the step answers shared without a
  walk: a deterministic program never pays the walk and never loses the
  sharing of an application to the bound.  The second part of the issue,
  `inner $## t` and `inner $!! t` on the Python backend (EvaluationSuspended
  and a ValueError in the issue; a hang here): N (fairscheme.py) runs with
  var.root = the redex when a step normalizes its argument ($!! and $##
  through _applyspecial), but its T_FREE branches copied the spine from
  rts.E along a path relative to var.root, which wrote the generator into
  the wrong slot, and its non-root T_CHOICE branch pull-tabbed the start of
  the walk instead of the choice at the cursor.  Fixed: the three copy_spine
  calls take rts.C.realpath, as hnf does, and the pull-tab takes
  state.cursor along state.realpath.  Tests: unit_setfunctions_bugs.py with
  data/curry/SetFunctionsBugs.curry (TestBoundVariable: the example of the
  issue, two set functions, the nested set function on the C++ backend,
  $## and $!!, the decided choice in the argument and in the function
  position, the walk behind a call, a forward node, a partial application
  and a data structure, an 81-element argument beyond the bound with the
  variable last, an 80-element one without a variable in a deterministic
  program and under a decided choice).  PAKCS cannot check the goals here:
  the pinned installation has no Control.SetFunctions (the setfunctions
  package is not installed), and the goldens of func_setfunctions.py are
  committed.  Relation to the dissertation (chapter 4, Figure 4.2, rule
  SF.1): a choice in the escape set always pull-tabs out of the capsule,
  and the two capsules it produces hold the alternatives themselves; no
  rule reads the state of an enclosing configuration.  Sprite's Queue::split
  leaves an undecided configuration in both queues without resolving the
  choice, and compensates with the refusal in choice_escapes and the walk
  of fork and read_fp through the queue stack; that refusal is where a
  capsule stops being configuration-neutral while its values live in the
  shared graph.  So the defect is an implementation deviation, not a flaw
  in the mechanics, and the fix above is the runtime's own invariant (state
  private to a configuration goes into a private spine, never into the
  shared graph) applied one step earlier; it is exact at creation time
  only.  Left, as test_narrowed_after_start (an expected failure on both
  backends): a capsule that starts while the variable is unbound, is
  consumed in part, and then resumes under alternatives that narrowed the
  variable differently (`let vs = valuesOf (set2 pickOrCase A u) in (head
  vs =:= A) &> ((u =:= (A ? B)) &> vs)` gives [A, A] twice; correct: [A, A]
  and [A, B]).  The clean repair is at the escape: split resolves the
  choice in the clones of the undecided configurations (LEFT in the kept
  queue, RIGHT in the new one), choice_escapes escapes every choice of the
  escape set that the nested configuration itself has not decided, and the
  nested fork and read_fp no longer need the enclosing fingerprints for
  those choices; the capsule becomes configuration-neutral and the
  creation-time walk goes away.  It touches queue.cpp, rts_setfunctions.cpp,
  rts_fingerprint.cpp, and allValues and filter_queue of the Python
  backend, and needs the set-function corpus and QueensSet (a split for a
  choice the outside decided produces a choice node the enclosing fork
  prunes at once).  To decide first: a choice captured through the function
  position (test_captured_argument of unit_cxx_scheduler.py, choiceCaptured
  here) is in no escape set; today the nested fork prunes it by the
  enclosing fingerprint, which gives (A, [A]) and (B, [B]) for
  choiceCaptured, the call-time choice reading of the shared x.
- Set functions whose values are sub-terms of the guarded argument (#32).
  set1 anyOf [1,2,3], set1 tails [1,2,3] and set1 (\xs -> head xs ? last
  xs) [1,2,3] asserted in procD or hnf on the C++ backend; the set guard at
  the root (2026-10-04) fixed it.  No code change; TestSubtermValues of
  unit_setfunctions_bugs.py pins the three forms on both backends.
- The shrinking pool (#36).  assignN 2 [1,2,3] [] with `let d = anyOf pool
  in ...` printed its six values, then aborted on the C++ backend
  (std::bad_alloc under the cap) and ran on on the Python backend.  A
  defect of the program: the rules `assignN 0 _ acc` and `assignN n pool
  acc` overlap, the front end compiles them to a choice, and the second
  rule at n = 0 recurses with n = -1, -2, ... without end, because nothing
  forces anyOf [] on an empty pool.  PAKCS 3.4.1 prints the six values and
  does not end either (60 s).  No runtime change; TestShrinkingPool pins
  the six values and the end of the guarded program (`| n > 0`) and the
  first six values of the program as written.  The issue should be closed
  with that note.
- show of a Float (#34).  The C++ backend printed 3.5 as 3.5000000000000000
  (std::showpoint with 17 digits in graph/show.cpp) and the Python backend
  used repr (1e+16, 1e-05, inf).  Both now follow PAKCS: the shortest
  digits that read back; the point stays in the digits from 1.0e-4 up to
  1.0e15 (or for a non-integral value), otherwise d.ddde<exp> with a signed
  unpadded exponent; an integral value ends in .0; -0.0 keeps its sign;
  Infinity, -Infinity and NaN (the spelling of KiCS2; PAKCS cannot make
  them).  show_float in graph/show.cpp (std::to_chars) and in show.py (the
  digits of repr); the unboxed branch of backends/py/currylib/prelude/
  show.py uses it.  Tests: unit_show_float.py (37 values, nodes, the
  Prelude's show, literals) on both backends; the REPL line of
  unit_goals.py.  Left: the C++ repr of a negative Float is <Float -2.5>
  where the Python backend's is <Float (-2.5)>; showsPrec 11 (-0.0) prints
  (-0.0) in PAKCS and -0.0 here (x < 0 is False for -0.0); the front end
  reads extreme literals inexactly (1.5e300, 5.0e-324,
  123456789012345678.0), in PAKCS alike.
- inspect.getimpl and geticurry (#38).  getimpl raised ValueError on the
  C++ backend, because CurryNodeInfo.getimpl read the source of a Python
  step.  Now the backend answers (IBackend.getimpl): the Python backend as
  before, the C++ backend with the generated step function read from the
  .cpp beside the shared object, or the disassembly of the bytecode under
  the interpret flag (backends/cxx/implementation.py); a constructor, a
  built-in, and a module without a product raise ValueError with the
  reason.  geticurry of a function of a module loaded from its product
  printed `exempt`, because the bill of materials (IModule.fromBOM) gives
  every function an IExempt body; inspect.geticurry now reads the
  ICurry-JSON file of the module once per module object and finds the
  function there; the .icurry attribute stays the placeholder, which the
  compiler and the type checker use.  Tests: testGetICurry, testGetImpl,
  testGetImplInterpreted of unit_inspect.py.  Left: getimpl(module) on the
  C++ backend emits a module whose .stepfuncs sections are empty
  (pre-existing).
- The C++ emitter (#59).  A cyrt::Variable assigned a bare literal or a
  nullary constructor (`_2 = false_();`) did not compile:
  vEmit_compileS_IVarAssign (backends/cxx/compiler.py) chose the cell form
  by the rendered text (Node::create alone), and every other spelling fell
  into `_N = <rhs>;`.  Since the pass-through change (T6) the lifted
  default of the front end is a plain pointer, so the example of the issue
  compiles at HEAD; the defect stayed reachable through hand-built ICurry.
  Fixed by the kind of the expression: an IVarAccess takes the indexer, an
  IVar copies the Variable, everything else takes the cell form; the output
  of every module that compiled before is byte-identical (FORMAT_VERSION
  stays 8).  Tests: unit_cxx_emit.py with CxxEmit.curry and the hand-built
  module CxxEmitHand, written as ICurry-JSON.  Left: the Python backend
  cannot scrutinize a Variable assigned a new node (vEmit_compileS_ICaseCons
  calls .hnf on a raw Node), unreachable from the front end, an expected
  failure there.
- Review of the batch, applied: the decided-choice case of #61 (above); the
  walk no longer enters generators and skips the deterministic case; tests
  for the coverage of the walk; lines over 79 columns wrapped.  Not done:
  the repair at the escape (the design above).  Seen on the way: the shared
  objects a test pool caches under .curry link the Prelude.so of the
  installation that compiled them, so a worktree built beside another
  checkout can map two Preludes (unit_cxx_literals.test_literal_count,
  unit_cxx_passthrough); ./run_tests --prepare in the worktree remakes
  them.  Considered: a timer instead of the step count for the rotation.
  Kept steps: the fairness of the scheme is defined in steps, a step count
  keeps the schedule reproducible across machines (the interruption tests
  of unit_cxx_scheduler.py scan 55 preamble lengths to hit the rotation
  window), a deadline is checked at the same safepoint and costs the same
  load and branch, and a queue of one configuration never rotates already;
  an adaptive period in steps is the lever for deterministic
  subcomputations.  A follow-up example: a toy build system in Curry
  (pattern rules as functional patterns, set functions to collect the
  rules that make a target, an inverse query with a free variable), with
  Python owning the file system and the actions.

2026-10-05: the velocity follow-ups (V5, the parallel prebuild, the runner)
- V5 (#45): the heavy test files are split by topic, and the test counts
  are unchanged on both backends.  func_eqconstr.py (767 programs) runs
  in six files: the hand-written programs (prog, iprog, cprog, fprog; 128)
  keep the old name, and the generated programs run by data type: a0 (180),
  a0b0c0 in three parts of 150, 150 and 144 (numbers 000-149, 150-299,
  300-443), and a2b1c0 (15).  func_math.py (218) runs in three: compare and
  sort (67) keep the old name, intmath (74) and floatmath (77) get files of
  their own.  unit_compile.py (11) splits into the compile API and its
  errors (6), the module registry (3), and the compiled expressions (2).
  unit_py_io.py (16) splits into the standard streams and the monadic flag
  (7) and files and errors (9).  The driver change: FILE_PATTERN of
  cytest.FunctionalTestCase takes a string or a list of globs; a source
  that two globs match is listed once (functional.file_patterns,
  functional.source_files).  unit_func_parts.py checks that the parts of
  each split corpus are disjoint and cover it once, with one test per
  source.  The runner knows the parts: CORPUS in prepare.py lists them,
  the corpus rule of --changed selects func_<dir>*.py, and the toolchain
  rule selects unit_compile*.py.  Measured on the warm tree, both backends,
  with four other streams on the machine (load 3 to 6 on 12 cores): every
  part takes 2.5 s or less; the manifest holds the 11 new entries and the
  4 refreshed ones, and no file lost its entry.  The longest files of the
  suite are unchanged: unit_examples.py (48 s, Python backend) and
  unit_cxx_toolchain.py (22 s, C++ backend).  The numbers of the issue
  (146, 118, 100 and 323 s) are cold-tree times: front-end calls with an
  empty ICurry cache, and one C++ compile per corpus program, which CI pays
  on every run.  Observed cold cost on the C++ backend: 0.47 s per object
  (45 objects in 21.4 s), so the largest part (180 programs) is about 85 s
  cold against 359 s observed for the whole corpus before the split.
  Found on the way: a make stage into the staged install during a test run
  replaced libcyrt.so under a running process, which crashed, and a missing
  Prelude.so failed 130 tests; streams that share an install must not
  rebuild it while tests run.
- Parallel prebuild of the Curry library (sprite-make --jobs).  sprite-make
  has a --jobs N option; auto is one job per processor.  It makes up to N
  modules at once.  Each module is made by a child process that runs
  sprite-make on that module alone.  The parent resolves the named modules
  and the closure of their imports, keeps the modules whose files are
  stale, and starts a child only when the children of its stale imports
  have ended.  A module whose files are current gets no child; it passes
  the jobs of its stale imports on to its importers.  The imports of a
  module come from its source (the lenient scan of curry.cache, with the
  implicit Prelude) or, without a source, from its JSON file; a cycle adds
  no edge.  The order is needed because the interpreter compiles a stale
  import on the spot when a module is imported, so two children would
  write the files of one module.  A failed child stops its importers ('X
  was not made because an import failed'); without -k no further child
  starts, and the running ones end by themselves.  With -o the one named
  module is made in the parent after its imports.  The output of a child
  is written when it ends, so the lines of one module stay together.
  Without --jobs, and with --jobs 1, the run is the serial loop as before.
  curry/Makefile passes the job count of make to the C++ step alone
  (PREBUILD_SO_FLAGS): PREBUILD_JOBS reads the -jN of MAKEFLAGS at recipe
  time (while the makefiles are read, MAKEFLAGS carries -j without the
  count), auto for a bare -j, else JOBS of Make.config, so make -C curry
  install follows the configured width too; JOBS=1 keeps the serial run.
  The ICurry and Python steps stay serial: 3.8 s and 1.5 s serial against
  4.4 s and 1.9 s with 12 children.  Measured with four other streams on
  the machine (1-minute load beside each number): the C++ step over the 13
  modules on a scratch copy of the installation, alternating, serial 56.0
  and 43.0 s against 31.0 and 33.8 s with 12 jobs (load 4 to 6); single
  samples 37.7 against 26.0 s.  Prelude.so is one translation unit of 28.4
  s (with the precompiled header) and bounds the step; the other twelve
  modules take 0.5 to 2.6 s each.  Full builds in place: clean 44.7 s
  before (load 3.4 to 3.9) against 39.0 s after (load 3.5 to 4.5), with
  48.5 s (load up to 7.0) and 60.9 s (load 6.4) under heavier load; a
  header change 36.9 and 32.8 s before against 31.9 and 31.7 s after (load
  4 to 5), 39.9 and 37.7 s at load 9; a no-op stage and a touch of
  node.hpp 6.1 to 6.4 s before and after.  The trees of the two codes
  differ in the member name of the precompiled header, libcyrt.a (archive
  order) and libcyrt.so alone.  Tests: TestJobs in unit_make.py (option,
  child command, import scan, graph with a current import and a cycle,
  imports first with the children recorded, width, serial path, failure
  with and without -k, -o, the bytecode of a current module) and
  test_prebuild_recipe_follows_the_job_count in unit_prebuild.py.  Next:
  Cpp2So compiles with -c and links separately, so ccache hits the modules
  and the Prelude compile overlaps the runtime build; the code generator
  could split the Prelude into several units; the children of sprite-make
  are outside the job server of make; the prepare pass of the test runner
  could pass --jobs; CompilationPipeline.rst could mention the option.
- The runner follow-ups.  Section 10 of tests/README was edited in place.
  The rules decided: (1) Fast tier: --fast keeps the files below S seconds
  on every backend of the run and the files without an entry, minus the
  functional tests and the other files that compile a corpus of their own
  (the files CORPUS in lib/testrunner/prepare.py names: unit_benchmarks.py,
  unit_cxx_variable.py; selection.compiles_corpus).  The manifest measures
  a warm tree; on a cold tree such a file first compiles its corpus (the
  math corpus about 90 s on the C++ backend against 3 s warm).  Run them
  by name: run_tests 'func_*.py'.  Tier sizes before and after: cxx 71 to
  52 files, py 74 to 55, both 69 to 50.  (2) Cap: twice the manifest peak,
  at least 1 GB (MIN_CAP; covers the toolchain inside the test process on
  a cold tree; before: 512 MB).  While a process of the Curry toolchain
  runs in the session (TOOLS in procs.py: the front end, icurry or PAKCS
  on swipl, the C++ compiler and linker) the cap is at least 2 GB
  (TOOL_CAP); icurry on swipl alone takes about 1 GB (unit_curry2icurry.py:
  131 MB with a warm ICurry cache, 1042 MB cold).  The line of the file
  notes the allowance; the manifest records the peak.  The budget counts
  the manifest caps, not the allowance; the free 40 percent of
  MemAvailable absorbs the compilers of a cold tree.  (3) Prepare pass:
  advisory.  sprite-make runs with -k; a directory whose module failed
  ends 'incomplete', not 'FAILED'; its line names the modules without a
  product (the .py or .so under the directory sprite-make -S names) and
  its log; one summary line follows the pass ('prepare: 37 modules in 1
  directory on cxx, every product present'); the last line of the table
  adds 'prepare: N of M incomplete'; the exit status ignores the pass.
  (4) perf: a shell between perf and the command reports a signal death as
  128 plus the signal, which the harness maps back; nothing is parsed from
  perf's text (measure.py docstring).  (5) test_committed_manifest_loads
  compares the shape of the manifest, not its numbers.  A cold copy of the
  tree ran the fast tier of 52 files on cxx in 112.9 s at -j 2 --mem 4
  (load 5 to 6).
- The review of the three items, applied.  (1) Under the interpreter flag
  interpret set to new or all, sprite-make --so ends at the JSON
  (Json2Cpp.ends_plan), and the prepare pass looked for the .so alone: a
  directory that did not end well listed every module as without a
  product.  prepare.product_suffixes names the JSON as the product of the
  C++ backend under those flags, read from the environment of the job
  (test_products_under_interpret).  (2) The man page of --jobs said that
  no two children write the files of one module; the order covers the
  files sprite-make writes, and the interface files of the Curry front end
  (.fint, .fcy) are the exception: the front end writes those of an import
  on its own when they are missing (the docstring of toolchain/_frontend.py;
  the staged install holds Prelude.fint though the repository commits
  none), so two children whose modules import a module without them write
  them at once.  The man page names the exception and --jobs 1 for the
  front-end step of such a tree; the comment of curry/Makefile says why
  the prebuild is safe: its ICurry is committed, so the children of the
  C++ step never run the front end.  (3) perf_status read an exit status
  of 255 as 'killed by signal 127'; now only a status that names a signal
  of the machine (signal.valid_signals) reads as one, and 255 stays 255,
  through the shim as well.  (4) This entry; the README and
  selection.compiles_corpus named func_math.py (2 s warm, 90 s cold) where
  the 90 s belong to the whole math corpus, now in three files of 1 s
  each.  (5) The skip reason of test_writeFile_keeps_prefix_across_rotation
  in unit_io_bind.py names unit_py_io_files.py, where the split moved the
  test; unit_func_parts.py follows the pointer.  (6) The floor of the cap
  costs width where the budget binds, because the budget sums the caps: a
  budget of 3.6 GB admits three files at the floor against nine at twice a
  200 MB peak (test_floor_costs_width_where_the_budget_binds); on the
  calibration machine the core count binds first.  Named in __init__.py
  and the README; the floor stays.  (7) sprite-make --jobs over a tree
  without the precompiled header started N children that each built it;
  the parent now builds it once before the first child starts
  (Cpp2So.prepare_header, called by _prepare_shared for a plan that
  compiles objects; test_header_is_built_before_the_children keeps the
  header in a directory of its own).  (8) _buildplan ran outside the error
  handler, so an error of the plan was a traceback; it runs inside it now,
  'sprite-make: message' and status 1, with -k too
  (test_plan_error_is_reported).  (9) Skipped: the 15 manifest entries of
  the split files were measured under load 4 to 5 (0.5 to 2.5 s, peaks 32
  to 159 MB, under the floor); they stay until the recalibration on a quiet
  machine at the integration, as the calibration paragraph prescribes
  after a split.  Changed outside the files the findings name:
  src/python/backends/cxx/toolchain.py (prepare_header), curry/Makefile
  (the comment), tests/lib/testrunner/selection.py (one docstring),
  tests/unit_func_parts.py (the pointer test).
  Verification of the three items with the review applied, through the
  runner at -j 2 --mem 4 on both backends, with other work on the machine
  (1-minute load 3 to 9 on 12 cores).  The unit suite without --prepare:
  138 of 138 run in 494.7 s wall, 1 failed (unit_cxx_literals.py,
  test_literal_count: 321 literal nodes where the generated text has 6)
  and 2 crashed with signal 11 (unit_sigtable.py, unit_cxx_passthrough.py),
  all on the C++ backend, and the three fail alone as well.  The cause is
  the tree, not the code: the worktree was mirrored with the products of
  the test corpora, and 312 of the 1193 compiled modules under
  tests/data/curry had been compiled by another checkout; their shared
  objects name the Prelude.so of that other installation by absolute path
  (readelf -d: NEEDED), so a test process loads two Preludes.  The ABI
  stamp does not see it: it digests the runtime headers and the flags, and
  the headers are the same.  With those products set aside, every product
  of the pool present after a prepare pass (95 modules in 2 directories,
  every product present, 74.5 s), 138 of 138 run, 0 failed, wall 525.3 s;
  the longest files: unit_examples.py 70.1 s on the Python backend and
  unit_prebuild.py 41.0 s on the C++ backend.  The functional suite with
  --prepare, outside the sandbox for the oracle: the pass remade the
  corpora (1175 modules in 11 directories on cxx+py, every product
  present; 100 s on the C++ backend, the math corpus 28.5 s and kiel
  23.5 s), then 40 of 40 run, 0 failed, wall 186.2 s; the longest files:
  func_flat2icurry.py 23.6 s on the Python backend and
  func_goal_defaulting.py 22.1 s on the C++ backend.  Follow-up: a shared
  object whose NEEDED entries name another installation should count as
  stale (Cpp2So.is_stale could read the Prelude path it was linked
  against, or the stamp could digest the installation prefix), so a
  mirrored or copied tree recompiles instead of loading two runtimes.
2026-10-05: typed boundary, stage 2 (epic #48)
- Y6, the engine (issue #55).  Done: curry.typecheck.engine types one
  Python-built expression over the schemes of the front end, Algorithm J
  without levels: union-find variables (terms.Var), unification with an
  occurs check and the rule for Prelude.Apply (unify.py), whnf folds an
  Apply whose head became a constructor up to the declared arity of the
  type and writes a saturated (->) as a function type, so return 5 is IO
  Int; a nested ForallType is instantiated when reached.  A Problem walks
  a tree of specs (App, Lit, Free, Known, List, Tuple, Iter, Typed) with
  an explicit stack, the expected type flowing down; the predicates sit on
  the variables and are reduced through the instances the moment a
  variable is bound (instances.py: the instance map of the table, the
  superclass closure from the _super#C#S selectors, so (Num a, Fractional
  a) prints Fractional a).  solve applies the ambiguity rule of the front
  end (a constrained variable absent from the result type and the
  environment defaults to Int or Float when a class is numeric, else an
  error with the front end's words), records the undefaulted scheme (what
  :type prints: fromIntegral 3 :: Num a => a, show [] ambiguous), applies
  the table of Y4 (default_scheme reused) until no predicate is left, and
  resolves every predicate to a DictTerm (the _inst# partial application
  with its context dictionaries); a free marker carries no constraint of
  its own, gets the Data dictionary of its type when ground and the Bool
  placeholder otherwise, and a function type is the catalogue's error.
  Newtype constructors (TypeNew entries) are erased: Identity 5 builds 5,
  a bare Identity builds id.  exprtype.py parses a monotype without a
  context, resolves names through the loaded interfaces, reads synonyms
  from the type lines of the .icurry beside the .fint (the front end
  writes them expanded), and raises the catalogue's errors with columns,
  declared arities and the interfaces searched.  errors.py holds the
  catalogue of design section 7 as CurryTypeError subclasses with the
  location, the symbol and both types; the undefaultable case stays Y4's
  DefaultingError.  engine.materialize is the reference materializer
  through raw_expr (dictionaries first, apply chains past the arity,
  lists in a loop, one unknown node per marker), an explicit-stack walk:
  a left-deep tree of 10^4 applications types in 0.13 s, solves in 0.25 s
  and builds in 0.33 s on the Python backend.  Tests: unit_typecheck.py
  (70 tests, both backends) with data/curry/TypeCheck.curry; the eight
  PAKCS cases are compared with the goldens of func_goal_defaulting.py.
  Left: read "5" raises the table's error with the oracle's sentence
  while the design's example 2 shows an 'ambiguous type in' line (the
  owner's call; see the review below); a literal under a type an earlier
  argument fixed is a conversion error that names that argument, and a
  nested literal under an expectation fixed above reports the missing
  instance; spec_of recurses over nested Python lists while the phases
  are iterative; zonk and the printer of a type are recursive, so a type
  deeper than the recursion limit (a Python list nested 10^4 deep) fails
  in solve.
- Y7, the typed curry.expr (issue #56).  Done: curry.expr describes its
  arguments as specs with an explicit stack, types them with the engine
  (TypedProblem, a Problem with the builder's own specs: a Curry node typed
  by a walk of its content, the wrappers fwd, set guard, constraints and
  anchor, references, unboxed payloads, the group of the keyword anchors),
  solves, and builds the nodes through make_node in a post-order walk: the
  dictionaries first in scheme order as partial applications with one
  missing argument, the arity of the symbol direct, the rest through
  Prelude.apply, a partial application for fewer, newtype constructors
  erased, literals at their resolved types (an int under Float is a Float,
  under a user Num type the fromInt route), a str under [Char] one string
  (the _biString node over the code points on the Python backend; on C++
  the list the step would build, made natively by the new binding
  Node.create_string, because the step unfolds the whole buffer at once
  and an owned buffer would need a finalizer in every collector), lists
  in a loop, iterators as generators whose items convert when demanded at
  the element type fixed at construction (a mismatch is an EvaluationError
  'cannot convert item X of the iterator at <location> to T', inside ? and
  inside a set function alike), free markers as one shared unknown node
  with the Data dictionary of their type (the Bool placeholder when
  polymorphic), explicit dictionaries before the value arguments taken as
  the dictionaries (the scheme then without its context), a lone symbol
  without a scheme and a bare node built untyped (module.goal of a saved
  module, raw graphs), and the result type recorded weakly by node identity
  (the Python Node gained a __weakref__ slot).  The walk is iterative with
  a memo per node, capped at 100000 nodes with the catalogue's error;
  curry.typed(node, 'T') and the record of a node curry.expr returned skip
  it.  API: exprtype= on expr and eval, curry.typed, curry.typeof (the
  undefaulted scheme or the defaulted type of a node, a symbol, a
  description or any argument), curry.describe (a Description, typed when
  a consumer asks; the same description twice is one node),
  curry.free(exprtype=), topython(value, exprtype=) threading the type
  through lists and tuples so that an empty [Char] is '', and curry.eval
  hands the recorded type of its goal to the converter.  The flag
  typed_expr (default True) switches expr to the untyped builder, under
  which goals.make_goal keeps the stage-1 check_application;
  expressions.untyped_expr serves compile(mode='expr'), whose expression
  module is not in the registry.  Costs, measured once on this machine:
  curry.expr(Just 5) 70 us on the Python backend and 78 us on C++ against
  7 to 10 us untyped; a list element 17 us (py) and 19.5 us (cxx) against
  14 us; 55 percent of a call is Problem.solve (two scheme constructions
  and the defaulting table), so an engine-side cache or a cheaper path for
  a problem without live predicates would halve it; the item converter of
  an iterator makes one builder and problem per item outside the five fast
  paths, about 100 us per item.  Tests: unit_typed_expr.py (both
  backends); testApply of unit_prelude.py and testConvertEmptyString of
  unit_py_conversions.py rewritten without expectedFailure; unit_goals.py
  expects the values the builder gives and pins the old check under the
  flag; unit_sigtable.py pins that raw_expr reads no interface and expr
  reads the Prelude's.  The report of the item blamed a runtime fault at
  module unload for a segfault of unit_sigtable.py on the C++ backend and
  for 321 literal nodes in unit_cxx_literals.py; both were stale shared
  objects compiled under another installation (the review below).  Left:
  the owner's word on the eager C++ string against the owned buffer of the
  design; the cost of solve; eval forwards its keywords as anchors now, so
  a misspelt keyword builds an unused anchor in silence.
- Y8, twins and the engine oracle (issue #57).  Done:
  curry.typecheck.serialize prints a spec tree as Curry text in two
  passes with explicit stacks: operands without text (Curry nodes,
  iterators, dictionaries) become the parameters p1, p2, one per node
  identity; free markers become `let x1, x2 free in` or, lifted,
  parameters, as the REPL lifts where x free; anchors become one
  recursive let (a keyword anchor keeps its name when it is a free
  identifier); Prelude names print unqualified, other names qualified,
  the special constructors as syntax, operators infix with parentheses
  by precedence, literals with Curry escapes, floats with a decimal
  point, a one-character string as Char or String by the type the engine
  resolved, a boxed literal over an unboxed payload as (3 :: Int), fail
  as failed; None, set guards, constraints and 1-tuples have no text.
  tests/func_typed_expr.py holds the corpus: the value twins in four
  generated modules (literals and the conversion rows of design section
  3.7, the library modules with seven twins whose leaves are values of
  an earlier curry.eval or an iterator, free variables and anchors, a
  module with a newtype, a Num instance, a class and unsigned bindings)
  and 18 type-only goals in TypeCorpus.curry; the functional driver
  evaluates the Python call in place of the goal against the goldens of
  tests/oracle with zero value differences on both backends; the type
  oracle compares the engine's undefaulted scheme with the .fint scheme
  of the goal in a canonical form (zero differences; a twin with markers
  compiled once more through the text route with the markers lifted);
  the conversion rows round-trip through curry.compile(mode='expr'); the
  committed modules must equal the serializer's output (python
  func_typed_expr.py --write regenerates them).  Observed on the oracle:
  PAKCS erases newtypes at run time as Sprite does (Box 5 prints 5), its
  REPL wrapper rejects a goal whose type names Data.Functor.Identity.
  Identity, so Identity 5 is reached through show, runIdentity, == and
  fmap; function values print in the REPL's internal form; 1.0e+20 and
  0.0 for a denormal; the Prelude of PAKCS 3 exports toInt; PAKCS 3.4.1
  prints truncate 2.7 as 3 and properFraction 2.5 as (3,(-0.5)) while
  both backends of Sprite truncate toward zero, so those two stay out of
  the value twins.  Left: the error printer still uses engine.describe
  with its depth cap (no hook); the empty string prints [] raw; the Float
  rows pass under the driver's float standardization and test_float_text
  pins the raw difference on C++ until #34 is fixed; an oracle script
  that passes :add Data.Functor.Identity would let Identity 5 itself be a
  value twin.
- Y9, documentation and the example helpers (issue #58).  Done:
  docs/source/REPL.rst, a page for the REPL (python -m curry.tools.icy)
  with its line in index.rst: the commands :load, :eval, :type, :set and
  :quit, the command-line form and its exit status, the defaulting of
  :eval against the undefaulted :type, the oracle's sentence for toEnum 65
  and the annotated fix toEnum 65 :: Char, the bindings of where-free
  variables and the :type divergence (no lifting, on either system), the
  IO rule (putStrLn "x" prints x then (), PAKCS prints x alone), the error
  format.  Preface note [1] says type signatures are optional for goals and
  names the table.  Quickstart replaces the undefined-behaviour warning on
  curry.expr with the typed description (a method call, typeof, the error
  at construction) and leaves the warning with raw_expr.  UsingTheAPI gains
  the label building-expressions and the subsection Typed Expressions: the
  scheme source, typeof in both modes, exprtype and curry.typed, the
  conversion rules, the walk of a value with its cap, the one-call scope
  of inference with the curry.describe form, the Data rule for free
  variables, residuation of arithmetic on a free numeric variable beside
  #37 (x + 1 =:= 3 suspends on both backends, a conjunction that binds x
  succeeds in either order), the error lines as Python prints them,
  raw_expr with the undefined-behaviour warning and the flag typed_expr;
  the eval paragraph says curry.eval(M.addOne, 1) is typed like any
  expression; the conversions section states the str/String rule and
  topython's exprtype; the five subsections of Building Expressions use
  the dash underline, which removes four heading-level errors of the build.
  CurryPackage lists raw_expr, typeof and describe; EnvironmentVariables
  describes typed_expr.  The helper text(s) = list(s) is gone from examples
  12 to 18 and the inline list(name) from example 20, with the data
  wrappers of 15 and 16, the string helper of 18 and the ''.join of 13 (the
  typed eval returns '' for an empty String); the READMEs say that
  curry.expr converts a str by the parameter type, and the README of 13 no
  longer claims a recursion limit near 140 characters; every output is
  unchanged on the C++ backend and unit_examples.py passes on both.  Found:
  the command prefixes of the REPL (:e, :t, :l, :s, :q) failed with an
  empty error, because each handler asserts the full command name while
  resolve left the prefix in repl.command; commands.eval writes the
  resolved name back (test_repl_command_prefixes in unit_goals.py).  The
  design's one-call example curry.expr(plus, [plus, 1], 1.5) is ill-typed
  (Num (a -> a)); the documented form goes through Prelude.apply.  Left:
  the issue texts of #58 (the #39 comment, the #3 correction, the
  method-application issue) await the owner's approval; examples 21 to 23
  still pass names as list(name); the docs show floats through
  converter='topython' because the C++ backend prints 1.0000000000000000;
  the build's remaining ERROR lines are pre-existing (the sprite-exec
  usage and three docstrings), and the duplicate-object warnings of the
  Reference vary between runs.
- Review of stage 2 (the findings on Y6 to Y9).  Fixed: the ABI stamp of
  a compiled module (toolchain.object_digest) names the real path of the
  installation beside the headers and the flags, because a module links
  against the shared objects of its installation by absolute path; the
  test products of a worktree compiled under another SPRITE_HOME loaded a
  second Prelude.so beside the installed one, which was the segfault of
  unit_sigtable.py after a reset and the 321 literal nodes of
  unit_cxx_literals.py on the C++ backend, both gone once the stamp made
  those objects stale (unit_cxx_toolchain.py test_digest; one more stage
  compiles every object once).  The value walk types a partial
  application by the scheme of its head and the arguments it holds (the
  new binding Node.partial_head on C++), so apply (map not) 1 is refused
  at construction instead of dumping core; a method without its
  dictionaries stays an opaque leaf.  A one-character string under an
  open type is a weak mark for Char, bound at the end of phase 1 when
  nothing fixed it, so ['c', 'ab'] and 'c' ? 'ab' type as [String] in
  either order, a type that only a list can fill (f a of fmap) makes it a
  string (fmap ord 'a' is [97]), and a nullary type such as Int keeps the
  message cannot convert 'a' to Int; four twins joined the corpus.  The
  context of a scheme is printed in the front end's order, by the first
  type variable then by the class, (Num a, Fractional b) => (a, b),
  compared with expression_scheme.  The Python backend's round halves
  away from zero, as std::round and PAKCS do (testRound).  Keyword anchors
  are declared before any value is converted, so one may refer to a later
  one, as with raw_expr.  The location of an error is built with a loop
  and elided after eight positions, so a list nested 1500 deep reports a
  CurryTypeError of a few hundred characters instead of a RecursionError.
  curry.eval strips a top-level IO from the type it hands to topython, and
  the untyped heuristic of topython joins a list only when its elements
  are characters, so map fst over an iterator of pairs of strings returns
  the strings.  The pattern of test_iterator_item_mismatch names
  Prelude.reverse, where the iterator sits.  The error of the table for a
  constraint on the element type of an iterator gains a line that names
  curry.typed(iterator, '[T]').  occurs and free_vars visit a structural
  node once, and the walk refuses a value whose type would print with more
  than the cap's nodes (a pair nest that shares its node at every level),
  so curry.typeof ends with ValueTooLargeError instead of a hang.  A
  description carries its anchors into the expression that receives it
  (the keys join the outer build; the problem gives every anchor a type
  variable of its own, so a description typed twice starts afresh) and
  keeps the keyword exprtype; describe refuses target.  The error of a
  literal that fails through a missing instance names the argument that
  fixed the type (argument 1 fixed a := Int (from Data.Maybe.fromJust
  <value>)).  curry.eval forwards its other keywords to expr as anchors.
  curry.typeof of a lone symbol without a scheme raises the no-scheme
  error, while curry.expr still builds it untyped.  The Problem docstring
  names curry.typecheck.errors in full, the one Sphinx warning of the
  diff.  The docs say that a string is one node on the Python backend and
  the list of its characters on C++, and the lazy exports of
  curry.typecheck include the serializer.  Skipped: read "5" keeps the
  table's wording (DefaultingError with the oracle's sentence) against
  the 'ambiguous type in' line of design section 7 example 2, the owner's
  decision between the design text and the code; the eager C++ string
  against the owned buffer, the owner's decision as well.  Left for the
  gated items: Y10 (specialization of a saturated method call to _impl#
  and alias resolution, so a Python-built 1 + 2 prints plusInt 1 2; today
  the selector route through apply costs the 70 us above and prints the
  dictionary) behind a measured cost or the owner's wish; Y11 (the schemes
  as metadata of the generated modules in place of the .fint reads, with
  the format change of the ICurry interpreter, #31), which would also
  retire the 11 ms index of the Prelude interface and the interface copies
  beside the .icy; Y12 (the harness: corpus harvest, type-directed random
  trees, leaf mutations, the CPU-time guard) after Y8, which would exercise
  the engine beyond the hand-written twins; and the smaller leftovers
  above: the depth of a type against the recursion limit, the cost of
  solve, the error printer's depth cap, the empty string printed raw, the
  truncate and properFraction divergence of PAKCS, and the issue texts of
  #58.

2026-10-05: the primitives' names and the symbol table of a compiled module

- After the merge of stage 2, func_io's goal `error` failed on the C++
  backend under the tiered default with "bad Curry string", on the tree
  before the merge as well.  Cause: Module::link keys the symbol table of a
  compiled module by the name of each info table, and fifteen primitives
  behind externals carried the name of the Curry function that wraps them
  (prim_error was shown as "error", prim_showIntLiteral as
  "showIntLiteral", and so on).  So `Prelude.error`, looked up by name,
  resolved to the primitive without the `$##` of the Curry function, and an
  unevaluated (++) reached extract_string.  Only a caller that runs
  interpreted meets it: compiled code links the function directly, and
  under interpret:all the Prelude's tables are built from its ICurry with
  the Curry names.  The lookup by name also serves Python-built
  expressions, so curry.eval(P.error, <unevaluated message>) failed the
  same way.  Fix: every prim_X table is named prim_X (its Curry name; the
  name shows in a partial application and in traces, where PAKCS shows the
  Curry name too); Module::link asserts that a name maps to one table; and
  extract_string follows forward nodes, as Node::successor_node does, and
  names the offending node in its error.  Tests: unit_prelude.py
  TestErrorMessage (a module compiled from text raises the full message on
  both backends; the primitives' names on the C++ backend); func_io passes
  with the io corpus interpreted first; unit_cxx_tiered's
  test_nondet_io_argument_error, the mixed-mode gap of the B3 entry above
  (an interpreted module applying readFile of the compiled Prelude), passes
  and lost its expected-failure mark: it was the same defect.
- Two tests adjusted to the merged tree: func_typed_expr's Float-text test
  lost its C++ expected-failure mark (#34 is fixed), and unit_cxx_emit
  builds the goal of its hand-made module with raw_expr, because that
  module has no FlatCurry interface for the typed builder.

2026-10-05: residuation-style arithmetic over free Int variables (issue #37)
- Diagnosis.  The sendMoreMoney program of the test data suspended on the
  C++ backend and did not end within 120 s on the Python backend; PAKCS
  prints the solution in 2.6 s.  The cause is not in the runtime.  PAKCS
  runs the tool transbooleq over every FlatCurry file before it compiles
  it (currytools/optimize/BindingOpt, called from compiler.pl under the
  pakcsrc property bindingoptimization=fast): a Boolean equality whose
  value is required to be True becomes constrEq.  The guard of main and
  the guards of digit and nzdigit are such conditions.  Under PAKCS the
  digit calls bind the letters and the store slots by constraint, and the
  arithmetic conjuncts suspend on the unbound letters and resume as the
  letters are bound.  With -Dbindingoptimization=no PAKCS reports
  "Evaluation suspended" in 1 s, as the C++ backend did.  Sprite's
  toolchain had no such pass.  The front end writes
  _impl#==#Prelude.Eq#Prelude.Int, which is rigid (eqInt x y =
  (prim_eqInt $# y) $# x), so vm == nzdigit 'M' suspended on vm for good,
  and so did every other conjunct.  With =:= in place of == the C++
  backend solved the program in 0.1 s before any change: the residuation
  support of the runtime (the residuals of a configuration, ready() and
  the rotation, concurrentAnd_step) was not at fault, and PAKCS, the
  oracle, never ran the program as written either.  Facts of the probes:
  under PAKCS, x == 3 and x == 'a' with x free suspend as values and bind
  in a guard; a guard with an otherwise branch, an if-then-else, (||) in a
  guard, (/=) in a guard, and id $ x == 3 suspend; (&&) in a guard, not
  (x /= 3), solve (x == 3), (===), a polymorphic Eq dictionary, and a free
  list bind; (x == 1) & (y == 2) as a value is True and (1 == 2) & True as
  a value has no value, because (&) requires both arguments True whatever
  its result is required to be; not ((x == 3) & (y == 4)) and not (solve
  (x == 3)) as values suspend, and not (not ((x == 3) & (y == 4))) is
  True.  Sprite gives the same seventeen answers on both backends after
  the fix (tests/unit_flat2icurry_bindingopt.py, PARITY_RESULTS).
- Fix.  curry.toolchain.flat2icurry.bindingopt is a port of the fast mode
  of transbooleq: the walk over the rule with a required value (Any, AnyC,
  or a constructor), caseArgType (the second branch False -> failed, or
  the one live branch), the Prelude table of preludeBoolReqValues (&&,
  not, ||, &, solve, &&>) with argumentTypesFor and lubAType, the three
  shapes of an equality call (an instance method with its dictionaries
  first, a class method under apply twice, a default instance method
  under apply twice), the reduction of $ over a partial application, and
  the rebuild of every rule that holds an equality.  The lattice is the
  one of the analysis: the join of two different constructors is the set
  of both (a frozenset), not AnyC, and a set requires nothing of the
  arguments of a call below it.  The first port joined them to AnyC and
  took the join path of the table under it, so not ((x == 3) & (y == 4))
  and not (solve (x == 3)) as values gave False where PAKCS suspends; the
  review of the batch found it.  translate and
  translate_file take bindingopt=False; the build route
  (_frontend.flat2icy) passes True; the oracle tests and the icurry route
  do not, so the oracle stays what icurry wrote.  The command line has
  --bindingopt.  The key of the ICurry cache covers the pass through the
  digest of the port's sources.  A product written before the change is
  reused until its source is newer (the chain keeps the filesys.newer
  rule), so a module of the pool that needs the pass must lose its .icy
  first, or its source must be touched; the review of the batch found the
  pool product of sendMoreMoney in that state in the worktree of the
  batch (it was made before the pass; sprite-exec -m sendMoreMoney
  suspended), and a touch of the source made it again.  The overlay
  archive holds no product of that module.  No test of the suite imports
  it from the pool; the new tests compile their programs from text.
- The Python backend, two defects.  Its primitives on unboxed values
  (_unbox in backends/py/currylib/prelude/math.py) evaluated every
  argument to hnf_or_free before they suspended, so vm == digit 'M' ran
  the generator before it suspended on vm: ten configurations per letter,
  their product for the program.  The C++ backend
  (currylib/defs/unboxed.def) and PAKCS stop at the first free argument.
  Four letters took 101 s on the Python backend and suspend at once now;
  _unbox suspends at the first free argument, from the left.  With that
  alone the puzzle still did not end there, and the first report of the
  batch took it for a speed limit; the review found the cause: ($##)
  (apply_gnf in backends/py/currylib/prelude/apply.py) counted a free
  variable with a binding and no generator as a residual, and _make_ready
  (rts_control.py) releases a configuration whose residual has a binding
  at once, so the step suspended on the same variable again without end.
  show x after x =:= (3 ? 4) spun at full load on the Python backend
  (the C++ backend printed "3" and "4"), and the puzzle finished its
  search (54,557 forks, the count of the C++ backend) and then spun in
  the show of the first digit.  The step now suspends on the variables
  without any information (rts.is_void: no generator, no binding, not
  narrowed), the test of ensureNotFree.  The puzzle then takes about 7 s
  on the Python backend.
- Options not taken.  (1) Make == on Int narrow in the runtime: against
  the semantics of Curry; x == 3 with x free suspends as a value under
  PAKCS.  (2) Write the test program with =:=: hides the gap for every
  program that relies on the default of PAKCS, the idiom last xs | xs ==
  _ ++ [x] = x of the PAKCS manual among them.  (3) The full mode with the
  required-values analysis of CASS: an interprocedural analysis over
  FlatCurry, several hundred lines; the manual of PAKCS says the fast mode
  is sufficient in most cases.  (4) Apply the pass to the FlatCurry file
  on disk, as PAKCS does, so that the icurry route sees it too: needs a
  FlatCurry writer (terms.showterm may serve) and a rewrite step before
  icurry runs; then the overlay archive would carry the optimized files
  and the two routes would agree again.  Open, with the committed ICurry
  of the Curry library, which predates the pass.
- Tests: tests/unit_flat2icurry_bindingopt.py, 28 tests on both backends.
  TestPass: the pass on FlatCurry terms (the guard, both branch orders,
  if-then-else kept, && and & in a guard, & as a value, && as a value
  kept, || kept, not (/=), /= kept, solve, not over & and over solve as
  values kept and not (not e) transformed, the lattice, $, the three call
  shapes, a dictionary argument, === and /==, equivalence off, a let
  binding, Free, Or and Typed, case_arg_type, the table, transform_prog),
  the flag of translate, and --bindingopt on the FlatCurry of
  sendMoreMoney.  TestRoute: one generator, two generators, the seventeen
  shapes with the answers of PAKCS, and sendMoreMoney as written on both
  backends (0.1 s on the C++ backend, about 7 s on the Python backend).
  tests/unit_py_residuation.py, both backends, selected by the --changed
  rule of the Python backend: TestSuspensionOrder (v == loopInt 0, v +
  loopInt 0 == 1, and four letters under let-bound equalities suspend at
  once) and TestGroundNormalForm (show x, show x ++ "!", and id $## x
  after x =:= (3 ? 4), show x after x =:= y & y =:= 5, and show x after
  x =:= y alone suspends).
- Open.  The icurry route and the overlay archive lack the pass (option
  4); AGENTS.md now says which settings tell the two routes apart.  A
  product written before the pass is reused until its source is newer:
  no mark of the translation that made an .icy file exists (the cache key
  covers the pass, the chain on disk does not), and a mark cannot tell a
  product of the old port from the pinned ICurry of the library, from an
  extracted product of the overlay, and from a product of the icurry
  route, which carry none.  A mark written from now on by the build route
  (the digest of the route beside the .icy, as the interface files travel
  with it) would cover the next change of the translation; left to a
  decision.  The copy of the program under
  tests/data/curry/benchmarks/failing was not run again.  The committed
  ICurry of the Curry library predates the pass.
- Verification, one file per process under prlimit --as 6 GB.
  unit_flat2icurry_bindingopt.py: 28 tests OK on py (15 s, the puzzle
  included) and on cxx (1.4 s).  unit_py_residuation.py: 3 tests OK on
  both backends.  Unchanged files, before the review: unit_flat2icurry.py
  68 OK (2 skipped), unit_curry2icurry.py 12 OK (1 skipped) on both
  backends, unit_cache.py 28 OK, unit_py_constraints.py 4 skipped,
  unit_py_evaluation.py 6 OK, func_residuation.py 7 OK on both backends.
  PAKCS 3.4.1 on the three shapes of not as a value: notConcurrent and
  notSolve suspend, notNotConcurrent is True; guardedConcurrent is (3,4).

2026-10-05: the fix batch (issues #35, #37, #66) and its review
- Three items fixed on branch wt/fixes, in one worktree, with a review of
  the diff before the merge.  The entry of #37 (above) holds its details;
  this entry holds the causes and fixes in short, the findings of the
  review, and what remains.
- #66, staleness by change time.  Cause: filesys.newer compared
  os.path.getctime, the change time of the inode, and
  _findcurry.currentfile starts a module from filesys.newest of its chain,
  so a chmod, a rename, a hard link, or the prefix rewrite of conda decided
  which product was newest; an installed .cpp could count as newer than
  its .so with a matching ABI stamp, and the first import compiled the
  module again.  Fix: newer compares st_mtime_ns; make overlay extracts
  with tar -m, so the products (archived at 2000-01-01) are newer than the
  sources of the checkout, and overlay-prune stays for the sources that
  changed since the archive.  Tests: unit_findcurry.py (a chain whose
  change times are reordered by chmod, and a real module built with the
  plan of the backend, on both backends) and unit_cxx_toolchain.py (the
  chain, not the object alone, set older than libcyrt).  Remains:
  conda/README.md open question 3 (path-free generated sources); the kept
  shipped object is an expectation until a package is installed under the
  new rule (the README says so now); the test products copied into the
  worktree have fresh times in arbitrary order, and the first run makes
  them again, as before.
- #37, residuation over free Int variables.  Cause: no binding
  optimization in the toolchain (PAKCS applies transbooleq to every
  FlatCurry file), and two defects of the Python backend, the order of the
  suspension of a primitive and ($##) over a bound variable.  Fix: the pass
  flat2icurry.bindingopt on the build route, _unbox suspends at the first
  free argument, apply_gnf suspends on the void variables alone.  The
  entry above has the details and what remains.
- #35, curry.save without a goal.  Cause: save wrote a footer with
  goal=None, and moduleMain of the saved program (src/python/__main__.py)
  built its own argument list and never read sys.argv, so -g was ignored.
  Fix: save without goal= raises ValueError (pass goal=NAME, or
  module_main=False for a library; commit 1233abf8), and the saved program
  runs curry.__main__.SavedMain, a Main with the switches igpstS: the
  recorded goal is the default of -g and keeps its recorded scheme, another
  goal gets its type from the module interface, -g '' runs nothing, --help
  lists the switches, and -b, -m and a file argument are rejected with
  status 2.  Tests: unit_goals.py TestSaveRefusals (both backends) and
  TestSave.test_command_line (Python backend, compared with sprite-exec);
  unit_examples.py runs 04-static-compile against expected.out on the
  Python backend.  Remains: a goal other than the recorded one with class
  constraints needs the module interface beside the recorded source path
  (Y11 would close it); a legacy saved file with goal=None now honours -g
  and runs nothing without it; the set of 04-static-compile stays {'py'}.
- The review of the diff, seven findings, all applied.  (1) ($##) on the
  Python backend (the entry of #37); the puzzle test runs on both backends
  now.  (2) The lattice of the pass: the join of True and False is a set,
  not AnyC.  (3) The pool product of sendMoreMoney made before the pass:
  made again by a touch of its source; a general mark is open; the claim
  of the finding that the overlay archive holds an unoptimized
  sendMoreMoney.icy did not hold, the archive has no product of that
  module.  (4) The tests of the Python backend moved to
  unit_py_residuation.py, which the --changed rule of the backend selects.
  (5) AGENTS.md names the two settings that tell the routes apart.  (6)
  The entries of #35 in this file.  (7) conda/README.md states the kept
  object as an expectation with its condition.
- Verification through the runner (-j 2 --mem 4), both backends:
  the named files on py (unit_goals.py 44 tests, unit_loadsave.py 4,
  unit_flat2icurry_bindingopt.py 28, unit_py_residuation.py 3,
  unit_findcurry.py 9, unit_examples.py 20): 6 of 6 OK; the same six and
  unit_cxx_toolchain.py (32) on cxx: 7 of 7 OK; the fast tier on py: 59
  of 59 OK; the fast tier on cxx: 56 files, 48 OK, 5 failed
  (unit_cxx_stack.py 3 errors, unit_cxx_literals.py 1 failure,
  unit_cxx_partial.py 1 error, unit_io_bind.py 7 errors, unit_inspect.py
  2 failures) and 3 crashed (unit_cxx_scheduler.py, unit_cxx_bom.py,
  unit_chars.py).  The eight fail the same way alone, and the same way
  with the Python package of the base commit 966196bf on the runtime of
  the worktree (a scratch copy of the package with the extension module
  of the install; unit_io_bind.py gives the same seven errors there), and
  unit_io_bind.py passes against the reference install of the head of
  py314: the failures predate the batch, which changes no C++ file and no
  file of the C++ backend, and sit in the runtime of the base commit or
  in this install; the fast tier of the merged tree must run again.
  Three symptoms: a module loaded from its shared object after a reload
  of curry gives names that are not UTF-8 in IModule.fromBOM
  (unit_io_bind.py, unit_cxx_stack.py); the process dumps core
  (unit_chars.py in test_code_point_above_the_maximum, unit_cxx_bom.py in
  test_generated_file, unit_cxx_scheduler.py); a corpus module is found
  interpreted where the test expects its compiled object
  (unit_inspect.py, unit_cxx_literals.py, unit_cxx_partial.py), the
  condition of section 4 of tests/README when the prepare pass did not
  run.  Single files, one process at a time: unit_flat2icurry.py 68 OK
  (2 skipped) on py; unit_curry2icurry.py 12 OK (1 skipped) on both
  backends.
  --changed maps Makefile, src/python/__main__.py and
  src/python/utility/filesys.py to every test file (paths unknown to
  RULES), so its selection is the whole suite; the named files and the
  fast tier ran instead.  unit_currylib.py did not run: it removes
  products of the shared staged installation, which other streams use,
  and only a comment changed there.  tests/manifest.json has no entries
  for unit_flat2icurry_bindingopt.py and unit_py_residuation.py, and
  unit_goals.py has 44 tests now (38 recorded): a calibration run on a
  quiet machine adds them.
2026-10-05: G7a, the age and phase counters of the collector (Gate 0, #81)
- Stage 0 of the generational design (the perf-program note
  gc-generational.md, section 7): counters instead of estimates.  Done:
  (W0.1) every collection records the seconds of its phases (the roots:
  the configurations of the queues and the nodes Python holds; the trace;
  the block sweep; the registries: the free-variable tables, the
  generators, the queues and sets destroyed with their configurations),
  the nodes marked, the configurations pushed, and the queues and
  configurations destroyed; gc_counters() sums them, gc_last_collection()
  holds the last record, sprite-exec --stats and curry.stats print the
  sums as fourteen gc_ keys after failed_compiles, and SPRITE_GC_REPORT=1
  prints one key=value line per collection on stderr (it replaces the
  compile-time GC_REPORT).  The timers cost a clock reading per queue
  pushed and a few per collection.  (W0.2) The sweep copies the marks of
  a collection into an ``old`` bitmap of the block header and sets a
  ``young`` flag on a block that kept no node; the mark phase counts the
  old and the young nodes it marks; the bitmap is an input of the
  counters only, the trace is unchanged, and the verifier checks that the
  old bits lie within the allocated slots, on an explicit request and in
  the stress mode at every collection (a pass over the blocks, as the
  sweep makes).  gc_old_node_count() reads the bitmap.  (W0.3) The
  old-write counters: a test at every return of a step in procS (the
  redex; inline, the young flag of the block first, the bit of the slot
  second: src/cyrt/graph/gc/block.hpp, the layout of a block shared by
  blockheap.cpp and the scheduler) and at the slot sites of the design
  (the compressions of forward chains in Variable::skip,
  Node::successor_node and compress_fwd_chain, the generator slot and the
  bindings of a free variable, the string advance of writeFile,
  Variable::set_successor, Cursor::skipfwd, and the forward_to,
  set_successor and step entries of the bindings), counting the writes
  into old nodes, the distinct old nodes written (a ``dirty`` bit per
  slot, counted and cleared by the sweep: the unit a write barrier
  records) and the blocks touched per interval; a slot whose node is not
  at hand is looked up in a two-level heap map over the 47-bit address
  space (in_heap_map, a bit per block; a block above that space stays
  unmapped and uncounted), because the address may be a Configuration
  field, a local, or an interpreter cell.  Compiled in under
  SPRITE_GC_WRITE_COUNTERS (make GC_WRITE_COUNTERS=1); without the macro
  the three hooks are empty inline functions, so the default build has
  no call at the sites and the indexer stays inline in generated code.
  Some sites are in the installed headers, so the setting reaches the
  installation as sysconfig/cxx_gc_write_counters
  (config.cxx_gc_write_counters) and the generated modules through the
  compile flags and the ABI stamp (toolchain.gc_flags, object_digest):
  a switch compiles the modules again.  The cost on Tak1 (perf stat
  -e instructions:u, three runs per build within 0.01 percent, the
  module compiled): the base install 16,271 M, the default build 16,259
  M (-0.07 percent), the instrumented build 17,483 M, 7.45 percent over
  the base, 36 instructions per step, over the one-percent budget of the
  design: beside the redex test, every Bool result is a forward node to a
  pinned constructor, and the case that consumes it compresses the
  chain, so the slot sites run about once per tak call.  The first
  version declared the hooks out of line in every build; the review
  measured its default build at 17,097 M, 5.09 percent over the base,
  because the empty calls made g++ outline Variable::skip in the module.
  Found on the way: g++ takes the first precompiled-header member of
  cyrt.hpp.gch/ whose options agree with the compilation and never
  compares a member with the headers, so a compilation that defines a
  macro the current member was built without (this one, or
  SPRITE_GC_MPS) took a stale member of an earlier runtime, and the
  module was compiled against old headers; PrecompiledHeader.prepare now
  removes the stale members of the other flavors (remove_stale_members).
  (W0.4) The runs, through the harness with the modules compiled, two
  measured repetitions and one under perf, on a machine shared with four
  other streams (the counters are deterministic; the seconds are
  indicative): QueensSet9 at growth 8, 11 collections, 1.47 s in
  collections, 134 ms each: registries 71 ms (53 percent; 58,128 queues
  and 766,400 configurations destroyed per run, about 1 us each), roots
  35 ms (26 percent; 147,578 configurations pushed per collection), trace
  28 ms (21 percent; 268,009 nodes marked per collection), of which 16.6
  percent old; per interval 151,504 distinct old nodes written (341
  percent of the 44,379 old nodes marked at the next collection, 60
  percent of the 268,009 nodes old during the interval: the frontier is
  written and dies within it) by 224,555 old redex writes (a tail call
  rewrites one old node per step; ReverseHO: 400,974 writes into 8
  nodes) in 621 blocks.  SearchQueens: 20 collections, 1.02 s, 51 ms
  each, trace 67 percent, 49.8 percent old, 129,700 old nodes written
  per interval (67 percent of the old nodes marked, 34 percent of the
  population), no dead queue.  PermSort 0.23 s (3.0 percent old),
  QueensSet 0.11 s (9.7), Tak1 0.007 s (16.3), Queens10 0.013 s (78.6),
  ReverseHO 0.05 s (15.0), Last 0.003 s (10.0).  The growth factors 2,
  8, 16: QueensSet9 3.29, 1.47, 1.04 s in 29, 11, 6 collections, 60.1,
  16.6, 11.9 percent old, the old nodes written 47, 341, 464 percent of
  the old nodes marked and 29, 60, 62 percent of the old population, the
  trace 30, 21, 17 percent of a collection, the registries phase 0.69 to
  0.85 s per run at every factor (the per-run term of the design's
  model, measured; the design estimated 0.45 s); SearchQueens 7.32,
  1.02, 0.50 s in 75, 20, 10 collections, 83.8, 49.8, 30.6 percent old,
  16, 67, 130 percent written, the trace 74, 67, 62 percent, no per-run
  term (-0.01 s through growth 8 and 16) but a per-collection cost that
  grows with the nodes marked (98 ms at growth 2 against 51 at 8).  Peak
  RSS: QueensSet9 192, 323, 466 MiB; SearchQueens 191, 305, 470.  Gate 0
  on QueensSet9, per growth factor: the old share passes at growth 2
  (60.1 percent against 50) and fails at 8 and 16; the old nodes written
  fail at every factor (47, 341, 464 percent against 25; 29 to 62
  percent of the old population); the trace fails at every factor (30,
  21, 17 percent against 40); the instrumented build costs 7.45 percent
  of Tak1 against 1; the growth line of SearchQueens has no per-run term
  (pass).  Verdict: no-go, on the trace share and the cost, not on the
  old share; SearchQueens at growth 2 passes the three shape clauses (the
  yardstick question of the design, open question 1).  G7b to G7f close;
  G8 (the growth default: growth 16 takes 29 percent off QueensSet9's
  collector and 50 percent off SearchQueens's, for 143 and 165 MiB) and
  G9 (the destruction of dead queues, the largest term of QueensSet9, 0.7
  to 0.85 s per run) open.  The tables, the commands and the load caveat
  are in the perf-program note gc-stage0.md.  Tests:
  unit_cxx_gc_counters.py (the toolchain flags and the ABI stamp of the
  setting; the keys of the bindings, curry.stats and stats.GC_COUNTERS;
  the phases, marks and pushed configurations of an evaluation that
  collects; 2,000 abandoned set-function queues destroyed with their
  configurations; the old bitmap equals the marks of the last collection
  and the verifier accepts it; the writes into old nodes and the old
  nodes written of CxxGc.thunks when the runtime counts them; the report
  lines; the stress mode with the verifier; a nested collection from a
  Python callback), with and without SPRITE_GC_STRESS=1, on both builds;
  unit_cxx_heap.py pins the inline indexer and the absence of the hooks
  in a module of the default build (test_tak_indexes_inline, which also
  requires a current ABI stamp of the object); unit_cxx_toolchain.py
  pins the removal of stale precompiled-header members; unit_api.py,
  unit_cxx_counters.py and unit_cxx_mps.py pin the new keys and the
  flags (the regex of test_keys in unit_cxx_counters.py lacked swapped
  and failed_compiles).  Docs: the stats fields of sprite-exec and the
  switch of the build, SPRITE_GC_REPORT on the environment page, the gc
  README, tests/README section 4 (the write-counter build is manual; the
  CI job "write-counters" runs its tests on request), the help text of
  --stats.  The manifest entry of unit_cxx_gc_counters.py was taken on a
  loaded machine.  Found on the way, not fixed: unit_io_bind.py's
  test_readFile_nondet_name_is_an_error fails with "bad Curry string"
  when IOBind runs interpreted under the tiered default while the
  Prelude is compiled (a stale object right after a header change); it
  passes compiled and under interpret:all, and it fails the same way
  against the install of the base commit with the same stale stamp, so
  it predates this item.  Left: the split of the 7.45 percent between
  the redex test and the slot sites needs a third build; the heap map
  (256 KB of pointers, a bitmap per 4 GB region) is kept by the heap in
  every build and serves a conservative stack scan later; the
  compressions of Bool results are a cost of their own (a forward node
  per comparison) that an in-place write of a pinned constructor's
  heap copy, or a boxed Bool, would remove.

2026-10-05: O1, saturation of apply chains (#70)
- The pass saturate_applies (interpreter/optimize.py; the analysis in
  icurry/analysis/partials.py) runs before inline_aliases under the key
  <backend>.opt.saturate_applies.  An apply whose head is known becomes
  the call it builds.  A head is known when it is a partial application
  (IFPCall, or ICPCall of a constructor), a call without arguments of a
  nullary function whose body is one, or an apply whose head is known and
  whose call is a partial application, followed through at most eight
  nullary functions and applies (analysis.partial_head).  The last form is
  the body of a nullary function the pass has not reached yet (g = k 1
  with k = f defined after the use), so the result does not depend on the
  order of the functions in the module.  A call of Prelude.$, the
  Prelude's alias of apply (analysis.is_apply), is an apply to the pass,
  which runs before inline_aliases.  A user alias of apply (app f x = f x)
  is not: the source wrote the indirection, and a program may need it to
  hand a known function value to the apply of the runtime, which the pass
  would fold away (CxxPartial.curry does, for the shared function-value
  node and the partials of 32 to 35 arguments; a let of a bare function
  value does not survive the front end, so no variable can hold one).
  inline_aliases makes an apply of such a call afterwards, on a head the
  pass would have known.  The applies are rewritten
  from the inside out, one at a time (analysis.saturate): an apply of a
  partial that misses one argument is the saturated call (IFCall, or
  ICCall), one of a partial that misses more is the longer partial, and a
  saturated call is not a known head, so the applies after it stay; a
  saturated call that is itself an apply (ap = apply, then ap f x) is
  saturated again.  So one rule covers the exact, the under-saturated, and
  the over-saturated chain.  The arguments a saturation copies from the
  body of a nullary function are rewritten too.  The arity comes from
  the symbol table of the module of the head and must agree with the
  missing count of the partial: the runtime derives the missing count of a
  partial with arguments from the arity (Node::create_partial), and a call
  built from a partial that disagreed would have the wrong size.  A
  variable head, a call with arguments (the method selectors applied to a
  dictionary, 343 sites of the corpus), and a head held in a variable stay.
  The body of a nullary function is copied into the use site, as its step
  would build it there, once per use, so a choice in it stays one choice
  per use.  Each nullary function records the partial it unfolds to under
  all.unfolding as ICurry-JSON (analysis.UNFOLDING_KEY), so a module loaded
  from its compiled form tells it to its importers.  When the copied body
  names a symbol of a module the caller does not import, the module joins
  the imports.  Prelude: 942 apply calls, 263 with a known head (134
  whose head is a partial application or a nullary function, and the
  applies above them in a chain), and 3 calls of $ with a known head (chr $
  and lex $), become 679 apply calls, none with a known head, and no call
  of $; 120 nullary functions record an unfolding (115 of functions, 5 of
  constructors: userError = UserError, showChar, the pure and return of
  Maybe and Either), 117 distinct texts.  The heads are the _impl#
  methods that fall back to a class default (_impl#/=#Prelude.Eq#
  Prelude.Int = _def#/=#Prelude.Eq _inst#Prelude.Eq#Prelude.Int).  In
  Queens10, x /= q in safe was apply (apply (_impl#/= []) x) q, three
  times, and is _def#/=#Prelude.Eq _inst#Eq#Int x q; isdivs of Primes
  likewise, with _def#mod#Prelude.Integral inside.
- Staleness: the format stamps went to 11 (C++) and 5 (Python), so make
  stage and the first import write every cached module again, once (9 and
  3, then 10 and 4, were earlier forms of the pass in the working tree,
  never committed); a file of the old format is correct but slower, and
  its record tells the modules that import it no unfoldings.  The key of a
  pass changes no stamp by itself: a module compiled before the pass is
  current by its ABI stamp and by its format, and would run as it was
  compiled.
- Found on the way, one: the C++ loader built the IModule of a shared
  object with the metadata {cxx.shlib} alone, so the keys of the passes,
  which the record carries, never reached the module, and every pass ran
  again over the bodiless functions of every compiled module at every
  import (five passes over the 1670 functions of the Prelude).  The loader
  hands the keys of the passes on now (backends/cxx/loader.py), as the
  Python backend does: the Prelude import takes 0.027 s in place of
  0.055 s, and every program retires fewer instructions at start (Fib
  0.92x).  Before the fix the new pass had added about 80 million
  instructions to every process (Fib 1.03x), which the first measurement
  showed.  The merge key cxx.is_merged stays behind: the merge adds the
  exported primitives, which the record does not list (unit_prelude.py,
  unit_inspect.py and unit_io_bind.py failed with it restored).
- Found on the way, two: the record of a shared object names a function by
  the name field of its table, and fourteen primitives of the runtime
  carried the name of the Curry function that wraps them (showIntLiteral
  for prim_showIntLiteral; the show and read literals, error, putChar,
  readFile, writeFile, appendFile, ioError).  The two rows collided, in
  Module::link and in IModule.fromBOM, and the primitive took the
  wrapper's place in the symbol table of the loaded Prelude.  Compiled code
  never noticed: it binds the wrapper by its C++ symbol.  The pass copies
  _impl#showsPrec#Prelude.Show#Prelude.Int = showSigned _inst#Real#Int
  (showString . showIntLiteral) into the modules that call showsPrec at
  Int, and an interpreted module then named Prelude.showIntLiteral through
  the symbol table and got the primitive, which shows its argument without
  normalizing it: showsPrec 11 (-2.5) "" gave "(-apply (negate ...) (-2.5))"
  (unit_show_float.py).  The primitives carry their Curry names now
  (src/cyrt/currylib/prelude/{show,read,basic,io}.cpp); an unevaluated
  application of one prints as prim_showIntLiteral x.  The same collision
  was the "gap of the interpreter in the mixed mode" of the tiered entry
  above: an interpreted module that applied readFile of the compiled
  Prelude to a nondeterministic name reached prim_readFile, which raised
  ValueError on the unevaluated name; it reaches the wrapper now, whose $##
  raises the nondeterminism error, and test_nondet_io_argument_error in
  unit_cxx_tiered.py is no longer an expected failure.  The loader asserts
  for every module, the Prelude included, that the table, the record, and
  the ICurry agree on the name of a function (interpreter/import_/load.py;
  the Prelude was exempt, which let the collision through), so a new
  collision fails at load.  The import of the Prelude from its ICurry
  (interpret 'all', and the compile of the Prelude itself) then found one
  more table with a name of its own: the table of nonstrictEq said
  nonStrictEq (prelude/constr.cpp); it carries the Curry name now.  Tests:
  TestFunctionNames in unit_cxx_bom.py (the fourteen pairs, no two rows of
  the Prelude's record share a name, and every built-in of the Prelude
  carries the Curry name it is registered under) and TestPrivateSymbols in
  unit_optimize_applies.py.
- Found in the review, one: IString had no _fields_, so icurry.json wrote
  it with its full name and module and could not read it back, and an
  unfolding that holds a string literal (replace_static_strings runs
  before the pass: isVowel = flip elem "aeiou") raised TypeError at the
  import of the module and of every importer of its compiled form.  The
  Prelude has no such unfolding, so make stage and the suites did not see
  it.  IString carries _fields_ = ('value',) as the other literals do, and
  analysis.unfolding answers None for a text that does not read back, so a
  bad record makes a head unknown and nothing more.  Two: the pass was not
  a fixpoint.  A use before the definition kept its applies (main = g 2 3
  before g = k 1 and k = f: when main was reached, the body of g was still
  an apply), and the saturated call of one rule was not saturated again
  (ap = apply; ap k 10 3 ended as apply (apply k 10) 3).  The analysis
  understands an apply in head position and saturates a saturated call of
  apply again, and the pass rewrites the copied arguments; see above.
  Three: a chain through Prelude.$ stayed, and the acceptance count of the
  Prelude test counted Prelude.apply alone, so it did not see the three
  sites.  The pass treats a call of Prelude.$ as an apply, and the test
  counts them.  A first form treated every alias of apply so, and four
  tests of unit_cxx_partial.py lost their subject: app f x = f x of
  CxxPartial.curry no longer reached the apply of the runtime with a
  known function value (above).
- Measured with the harness on the C++ backend, 3 repetitions, other work
  on the machine, the deterministic columns deciding; the notebook of the
  performance program keeps the table, the records, and the commands.
  Steps: Queens10 54505073 to 44630642 (-18.1 percent), Primes 61364444 to
  49108670 (-19.97 percent), Tak1, Fib, and Reverse unchanged.
  Instructions: Queens10 0.79x, Primes 0.78x, Tak1 0.99x, Fib 0.92x,
  Reverse 0.96x.  Collections: Queens10 54 to 38, Primes 64 to 47.  CPU
  seconds, indicative: Queens10 1.81 to 1.49 s, Primes 2.19 to 1.79 s
  (the final run; the run before the review changes gave 1.41 and 1.78 s
  on the same steps and instructions, which the review left unchanged:
  none of the five programs has a chain through $; one run between them
  took 1.3 to 1.6 times the CPU seconds of either on every program, a
  state of the machine).  The gate of the issue, 20 percent of
  the steps, is missed by 1.9 points on Queens10 and by 0.03 points on
  Primes: the pass removes the nullary function and the two applies of
  each /= chain (3 of 13 steps per comparison, 9 of 46 per list element
  of safe), and the rest of the chain, == d through the selector and the
  dictionary, is a head with arguments, which O2 and O3 own (section 4.2
  (b) and (c) of the plan).
- Tests: unit_optimize_applies.py (25 tests on both backends, with the
  helpers of unit_optimize.py): the analysis on hand-built ICurry (the
  arity check, the depth bound of nullary functions and of applies in head
  position, a cycle, metadata before body, constructors, an apply in head
  position, ap = apply, $ as an alias of apply, the unfolding text of
  every literal and a text that does not read back), the shapes of one
  module (exact, under-saturated, over-saturated, nested, a nullary head
  that unfolds to a partial with arguments, a variable head, a head with
  arguments, a chain through a shared variable, constructor partials, the
  recorded unfoldings), the shapes of a second module (a use before the
  definitions, ap = apply, a chain through $, a user alias of $ that
  stays, a string literal in an unfolding), values and steps against the
  unoptimized program (the same text under another name, imported from
  its ICurry object with the key of the pass set, so the pass does not
  run), a choice in an argument of a collapsed chain (the values of the
  uncollapsed program and no more: in the argument, in the copied body,
  and through a shared variable), the unfolding across modules through
  the metadata of a compiled module (a string literal included) and the
  import join, the generated code of both backends, the installed Prelude
  and its ICurry (no apply or $ with a known head remains, measured after
  replace_static_strings and before alias inlining), the compiled Prelude
  not optimized again (skipped under interpret all), and the chains of
  Queens10 and Primes.  unit_cxx_partial.py reads the format stamp from
  the emitter instead of spelling it.  The manifest has no entry for the
  new file yet.
- Left: a head that is a call with arguments (== d; 343 sites), a
  variable head (146 sites; the applyN primitive of the plan), and a call
  of a user alias of apply on a known head, which inline_aliases turns
  into an apply after this pass (above).  Seen on
  the way, not of this item: on the C++ backend under interpret 'off', a
  module imported from its ICurry object gets tables without a step
  (backends/cxx/materialize.py, create_function) and no object, and the
  first evaluation of one of its functions ends in a segmentation fault
  (a probe: import Top from toolchain.loadcurry, then curry.eval of its
  main).  The tests import such a module for its shapes alone and take
  the values from a module imported by name.

2026-10-05: example 24, a build system (the decisions of make as a relation)
- examples/24-build-system, the opener of the talk narrative (the notes
  of 2026-10-05) and the example of section 12.2 of the phase 3 plan.
  Curry owns the decisions, Build.curry (about 150 lines): rulesFor is
  one equation per rule, the pattern rule %.o: %.c as the functional
  pattern stem ++ ".o", the library and the program by name, and the
  explicit rules of the project through free variables and =:= over
  anyOf; rulesOf collects the rules of a target with set2 and sortValues,
  and the rest of the module reads the rules as plain data; inputs
  follows the include table Python scanned through the headers with a
  seen list; stale is a relation over a table of (file, stamp) pairs;
  waves and layers are the topological order, a cycle fails the guard
  and the plan has no value; ready is the one-line scheduler; affected
  is the relation read backwards (t = anyOf (targets p) under the guard
  reads p t f); ambiguous is the lint, a target with two rules.  An
  explicit rule for a new name adds a target.  Python owns the world,
  go.py: a four-source C project in a temporary directory, include
  scanning by a regular expression, stamps as st_mtime_ns, a thread pool
  that submits the ready set and asks Curry again after each completion
  (Python keeps the running set and the outcome of each recipe), the
  recipes through CC or cc and ar with a stand-in that writes placeholder
  files when no compiler is on the PATH (the same stdout, a note on
  stderr), --trace, --jobs, and --serve PORT with http.server and a JSON
  endpoint.  The tested run: the full build, a build with nothing to do,
  a touch of shape.h with the prediction of affected (the build rebuilds
  exactly that set), a second rule for main.o (the lint stops the
  build), and a rule that closes a cycle (the plan has no value).  The
  docs page of the examples has its section; unit_examples.py runs it on
  both backends.
- Measured on this machine, under the 2 GB cap: warm ./run 0.42 to 0.46 s
  on the C++ backend (three runs) and 8.3 s on the Python backend (two
  runs); a first run from a copy without products, with the cache off,
  1.2 s and 8.7 s; without a compiler on the PATH 0.35 s and 8.2 s.  The
  Python backend answers a question in 0.1 to 0.7 s (the ready set on a
  built tree 0.37 s, the plan 0.71 s, the stale check of one target
  0.17 s; the C++ backend at most 0.03 s), and the run asks dozens of
  them (the build loop asks for the ready set once per completion), which
  is nearly all of its time.
- Found while writing it: a name that a functional pattern produced
  carries its narrowing into every later comparison.  A first version
  with object as a functional pattern and the rules read from rulesFor
  directly did not finish waves or plan within 120 s on either backend;
  with a plain object and the rules still read from rulesFor, plan after
  the touch took 10 s on the C++ backend for six targets; read through
  the set function rulesOf, whose values are plain data, every question
  takes at most 0.07 s on the C++ backend.  Whether the cost of a
  narrowed name in a deep lazy context is an issue against the runtime
  is open.
- Review, applied: the table the build loop hands to Curry omits the
  targets whose recipes run or failed (a compiler creates its output
  before it finishes; a wrapper that created shape.o early made libgeom.a
  ready and archived an empty object, and demo failed to link); stale
  and reads carry a seen list, so a cycle through existing files ends in
  the guard of layers instead of the stack limit (the first form hit the
  4 MB stack limit on a built tree); the driver reads the plan
  with next(values, None) and prints the failure; the lint gates
  run_build; touch waits until the header is newer than every other file,
  so the step does not depend on the clock of the file system; the
  README has the five sections of the series and no date; the explicit
  rules add targets.
- Waits for #61: the toolchain choice, toolchain = "gcc" ? "clang" with
  set functions over the targets, is the shape of the defect (a set
  function that starts while a variable is unbound and resumes under
  alternatives that narrowed it differently).  The fix of the bug batch
  covers a choice decided before the set function starts; the repair at
  the escape of the capsule is designed and not done.  When it lands, the
  example adds the choice and lets the lint report a project whose
  targets disagree on the toolchain.  Also open: a static cycle check
  (layers over every target, as make reports a cycle before it looks at
  a stamp) and a real project (the sources of a directory, the rules
  from a file, the includes from the compiler's dependency output).
2026-10-06: example 24 restructured, the makefile is the Curry script
- The owner's correction of 2026-10-06: the example was expected to write
  something makefile-like in Curry (CC = ..., object (stem ++ ".o") = ...,
  a small library imported, a makefile drop-in), and Build.curry with the
  project as a Python record did not show it.  examples/24-build-system is
  now three parts.  Make.curry, the library: data Rule = Rule Target
  [Target] [String] (an empty command is a prerequisite line), a makefile
  is a function Target -> Rule, rulesOf mk t = sortValues (set1 mk t)
  collects the rules of a target (the #86 note stays), build reads the
  makefile once from the goal down into the rules of every name of the
  build with the includes beside them (the database of make), and the
  questions read the build: inputs (the merged prerequisites with the
  headers through the include table), stale over the stamp table, waves
  and plan (a cycle fails the guard), ready, affected (anyOf over the
  targets under the guard reads), ambiguous (the rules with a command of
  a target that has more than one), rows for the driver.
  project/Makefile.curry, the makefile in the owner's shape: cc, cflags,
  ar, library, program, the object lists and goal as definitions, the
  pattern rule as the functional pattern rule (stem ++ ".o") = Rule ...,
  the library and the program as guarded equations with t as $@, one
  prerequisite line without a command; project/Makefile_ambiguous.curry
  imports it and adds a second rule with a command for main.o;
  project/Makefile_toolchain.curry binds the compiler as a parameter of
  the rules and lists makefiles = [(cc, rules cc) | cc <- toolchains];
  project/Makefile_choice.curry has cc = "gcc" ? "clang" inside the rules.
  The four C sources and two headers are real files in project/.  make.py
  is generic: -f, -C, -j, -n, --touch, --affected, --trace, --serve, GOAL;
  it imports the makefile from its directory (realpath, so a linked
  makefile compiles beside the original), passes rule as a function value,
  builds one Curry value of the build per process and shares it across
  the questions, scans the includes, reads st_mtime_ns, runs the ready set
  in a thread pool, prints the plan, the waves and the status, with the
  stand-in when a program of the plan is not on the PATH; a module may
  export makefiles, a list of named makefiles, and the driver plans each
  and builds with the first whose programs are on the PATH.  The run
  script copies the sources into a temporary directory, links the
  makefiles, and runs six steps: the full build, nothing to do, the touch
  of shape.h with --affected, the lint, the toolchain plans, the lint on
  the per-use choice.  README, examples/README and Examples.rst follow;
  unit_examples.py keeps 24 on both backends.
- Measured, under the 2 GB cap: warm ./run 1.89 to 1.95 s on the C++
  backend (four runs) and 7.5 to 7.6 s on the Python backend; a first run
  with the four makefile modules unconverted 4.5 s; without a compiler on
  the PATH 1.8 s; these with every module interpreted on the C++ backend;
  with the object of Make that sprite-make --so writes in 1.7 s, 1.6 s
  (two runs).  The questions of one process take 0.11 s on the C++
  backend and about 1 s
  on the Python backend (the read of the makefile 0.23 s, the plan
  0.35 s); the old design asked them per question at 0.1 to 0.7 s each.
  unit_examples.py: 21 tests on the C++ backend (2 skipped, 24 in 1.9 s)
  and on the Python backend (4 skipped, 24 in 7.5 s).
- Found: a module name with a hyphen is refused (ModuleLookupError), so
  the variants are Makefile_ambiguous and Makefile_toolchain.  A make.py
  process ends before the background compile of its makefile, which the
  runtime kills at exit, so the makefile modules and Make run interpreted
  on the C++ backend in a run of the demo; an object that a longer probe
  process had left without its ABI stamp did not load (the same 1.9 s as
  interpreted); the object with its stamp saves 0.3 s per run.
- Found, the cost of a makefile that carries a choice (the shape of #86
  one level up, and of the captured choice of #61).  The forms measured,
  on the design with the database and a fresh build per question:
  (a) rules ("gcc" ? "clang") built by curry.expr, or a plain let, or
  rule = rules "gcc" ? rules "clang" met first inside set1: the choice is
  encapsulated and both compilers land in one set (one merged plan; the
  lint fires), by the captured-choice reading of the #61 entry.
  (b) decided before the first set function, by a guard tc =:= toolchain
  on a free variable or by let tc = toolchain in tc `seq` rules tc: the
  right values (two consistent plans on the rules of two targets, 0.01 s),
  but every set function of the build meets the decided choice again:
  C++ backend, let and seq: names 0.05 s, targets 0.5 s, ambiguous 0.7 s,
  ready 0.9 to 1.4 s, waves not within the 60 s budget; guard: names
  0.12 s, targets 1.26 s, ambiguous 1.34 s, waves not within 60 s;
  Python backend: names 5.7 s (let and seq) and 11.9 s (guard), then the
  timeout.  rule = rules "gcc" ? rules "clang" with the choice decided
  first: std::bad_alloc under the 2 GB cap in names on the C++ backend;
  ready 4.7 s and waves not within 60 s on the Python backend.  On the
  first design, which asked the makefile per question: stale 0.06 s and
  ready 0.12 s, waves and plan not within 40 s on the C++ backend.
  (c) the per-use choice cc = "gcc" ? "clang" inside the rules: two rules
  for each of the five targets that read cc, 0.1 s on the C++ backend and
  0.3 s on the Python backend; this is step 6 of the demo.
  The example binds the compiler as a parameter and lists the makefiles
  (step 5: both plans in 0.6 s on either backend).  Open: whether a
  decided choice met again by a set function can take its side without a
  private copy of the spine, so that the bound-once form of (b) costs
  what the list costs.
- Review of 2026-10-06, applied.  The command of a Rule is one String, as
  a makefile writes it (data Rule = Rule Target [Target] String; the empty
  string is a prerequisite line); the variables are strings (cflags =
  "-O2 -Wall"), and the pattern rule is the owner's line with an
  as-pattern for $@: rule t@(stem ++ ".o") = Rule t [stem ++ ".c"] (cc ++
  " " ++ cflags ++ " -c " ++ stem ++ ".c -o " ++ t); the driver splits a
  command with shlex.split, still without a shell.  A missing source
  stops the build before the plan, as make does: Make.missing :: Build ->
  Table -> [(Target, [Target])] lists the sources without a stamp with
  the targets that read them, and the driver prints "no rule to make
  vec.c, needed by vec.o" beside the lint and exits 2 (before: the other
  objects were built, vec.o and its dependents stayed stale, exit status
  0).  Every error of the driver exits 2 through stop(), as make does
  (SystemExit(str) exited 1), and --touch of a missing file is such an
  error.  A target of the plan that never ran makes the exit status 2: a
  command that writes no output counts as run, as in make, but its
  dependents never become ready (probed: x built, y stale, exit 2).
  Makefile_toolchain.rules is rulesWith, so a module that imports Make
  and the makefile unqualified has no ambiguous name.  The run has eight
  steps: 7 plans Makefile_cycle.curry (shape.h <- demo) in the fresh
  copy, "plan: no value (the rules have a cycle)", exit 2, so the index
  entry "a cycle as a failure" has its step again; 8 builds a copy
  without vec.c.  README: "read once per build"; the excerpt "with its
  comments shortened"; the plain-let paragraph says what Sprite does with
  the captured choice (#61) and no longer calls the merged set the
  meaning of the form; the long sentences are split; the mapping says why
  CC is cc (an uppercase name is a constructor); the driver is "generic
  for a C project", the include scan being its one piece of knowledge of
  the language, in the README, examples/README and Examples.rst.  GNU
  make 4.3 checked on a two-line makefile: a missing source prints "No
  rule to make target 'vec.c', needed by 'main.o'.  Stop." and exits 2;
  a cycle prints "Circular shape.h <- demo dependency dropped." and the
  build goes on with exit 0; an unknown goal exits 2.
- Measured after the review, under the 2 GB cap, with a load average of
  about 2.7 on the 12 cores from other work: the eight-step ./run 2.68 to
  2.79 s warm on the C++ backend (three runs), 10.8 and 11.1 s on the
  Python backend, 6.2 s cold on the C++ backend (the library and the five
  makefiles converted), 2.7 s without a compiler on the PATH (placeholder
  files, stdout identical); sprite-make --so Make.curry 1.66 s, and the
  run with the object 2.28 and 2.34 s (the object was removed again).
  With a load average of 5 to 7 the same warm run took 5.2 to 5.5 s on
  the C++ backend and 11.9 s on the Python backend.  unit_examples.py:
  21 tests on the C++ backend (2 skipped, 24 in 2.7 s) and on the Python
  backend (4 skipped, 24 in 11.2 s); ./run_tests --changed -j 1 --mem 3
  --backend both: 2 of 2 ok.  The status page smoke-tested again after
  the change (/status.json, ?build=1, ?touch=shape.h).
- Left: a static cycle check (layers over every target, as make reports a
  cycle before it looks at a stamp); phony targets and a target without a
  command; a real include scan from the compiler's dependency output.
2026-10-05: set functions, the repair at the escape (#61, the general case)
- The defect left by the bug batch: a set function that starts while its
  free variable is unbound, is consumed in part, and resumes under
  alternatives that narrowed the variable differently shared the SetEval
  node and its queue; the nested queue read the fingerprint of the
  alternative that resumed it first (choice_escapes refused the escape of a
  choice an enclosing configuration had made, and the nested fork pruned it
  by that fingerprint), and the shared allValues node kept that value for
  the other alternative: narrowedAfterStart gave [A, A] twice.
- The repair, on both backends, is rule SF.1 of the dissertation (chapter
  4, Figure 4.2) at the escape.  A choice of an escape set always leaves
  the capsule: choice_escapes (rts_setfunctions.cpp, rts_setfunctions.py)
  reads no enclosing fingerprint any more, and no fingerprint at all; it
  escapes a choice that is in the escape set of the current set or of an
  enclosing set, unless the queue has escaped it before.  The split
  (Queue::split, split_queue) keeps a configuration that made the choice
  LEFT, moves one that made it RIGHT, keeps one that has not made it in
  both queues, as before, and records the choice in both queues
  (Queue::decisions, Queue.decisions), so a configuration meets it a second
  time without an escape: it forks, and the fork reads the side from its
  own fingerprint or, through the walk of the queue stack, from the
  enclosing configuration, which runs this queue because it decided the
  choice that way (the queue of a side is reachable through that side of
  the escaped choice alone).  The escaped choice is a node of the shared
  graph like any pull-tabbed choice: an enclosing configuration that has
  not decided it forks on it, and one that has prunes it.  A configuration
  that has decided it (itself, or an enclosing one did: read_fp) takes its
  side at once through a private copy of its spine and E_RESTART
  (allValues_step, allValues), as a binding goes into an expression; the
  fork would prune the choice node to the same side, but it sends the
  configuration to the back of its queue, and a search with many
  alternatives then advances them in lockstep with every capsule alive.
  The review of this entry (below) bounded that fast path: a configuration
  takes the side at once only when it owns the decision.  The capsule
  writes nothing into the shared graph that depends on the configuration
  that runs it; the walk of fork and read_fp through the queue stack stays,
  and it prunes by an enclosing decision only in a queue that an escape of
  the choice has bound to that side.
- Decided first, the captured choice (test_captured_argument of
  unit_cxx_scheduler.py, choiceCaptured of unit_setfunctions_bugs.py): the
  call-time choice reading stays.  A choice captured through the function
  position (set1 (constT x) 0 with x = A ? B) is in no escape set; the
  dissertation's SF.1 reads the escape set of the capsule alone, and a
  captured occurrence is unboxed, so the rule encapsulates it; Sprite's
  nested fork prunes it by the fingerprint of the enclosing configuration
  when that configuration decided it before the capsule reached it, which
  gives (A, [A]) and (B, [B]), and encapsulates it otherwise.  The goldens
  of applyS.curry (from KiCS2) pin the same reading: an argument applied
  with $< is encapsulated (goal1, goal4, goal30), and a captured occurrence
  follows the guarded one of the same choice (goal10, goal11).  PAKCS
  cannot check these goals: the pinned installation has no setfunctions
  package.  nestedCapture is the other half: the inner function captures
  the argument of the outer set function, so the choice is in the escape
  set of the outer set alone; SF.1 read literally keeps it in the inner
  capsule ([[1, 2]]), which puts the non-determinism of the outer argument
  inside a value of the outer set function.  A choice of the escape set of
  an enclosing set came through the guard of an argument of that set
  function, so it is from outside that capsule and from outside every
  capsule nested in it: choice_escapes reads the escape sets of the
  enclosing sets as well, and nestedCapture gives [[1]] and [[2]]; the
  expected-failure mark of test_captured_argument is gone.  goal10 of
  applyS.curry decided the last detail: a configuration that decided an
  escape-set choice itself (through the captured occurrence, before the
  guarded one put the choice into the escape set) still escapes it, and the
  split sorts it to its side; a refusal kept the two sides of the argument
  in one set.
- The creation-time walk (holds_private_state, _holds_private_state) stays
  unchanged.  The captured shapes need it: without the private copy the
  shared capsule of choiceCaptured or viaPartial keeps the values of the
  configuration that created it.  The guarded shapes (boundVar, choiceArg)
  no longer need it: their choice escapes, and a variable the capsule
  returns is resolved by each reader of the value; there the copy only
  loses the sharing of the capsule.  The walk could skip the guarded
  subtrees; not done, because the shapes that stay shared are exactly the
  ones the escape then has to handle, and the gain is one capsule per
  alternative in programs that narrow an argument before the set function
  sees it.
- Two forms were measured and rejected on the way (QueensSet9, C++
  backend, one repetition, the HEAD build 8be3209c of the same machine
  as the reference: 12146105 steps, 2979683 forks, 11 collections,
  766038 configurations destroyed, 58100 queues destroyed, 18.17 G
  instructions, 338 MB peak).  A split that clones every undecided
  configuration at once, LEFT in the kept queue and RIGHT in the new one:
  923263 configurations destroyed, 461 MB, instructions +6.2 percent; the
  clones of the side nobody runs are the cost, where the lazy clone at the
  step (unshare_front) never paid for them.  A lazy split that writes the
  side into the fingerprint of the clone: steps 12146105 to 12328580 in
  its first form (a configuration cloned by one queue stayed undecided in
  the other, which held it alone and escaped the choice again), and with
  that fixed through a count of the decisions applied, 19.67 G
  instructions (+8.3 percent) and 413 MB: the writes of a sparse id into
  every nested fingerprint, which the walk made unnecessary in the first
  place.  The final form writes nothing: the queue records the choice,
  choice_escapes looks it up (a vector of the escaped choices of the
  lineage, 36 entries at most here).
- Measured with the harness (C++ backend, one repetition, other work on
  the machine, after against the HEAD build): QueensSet 2152241 steps,
  511428 forks, 3 collections; QueensSet9 12146105, 2979683, 11; PermSort
  12531076, 1588167, 9: all three equal before and after, the
  configurations and queues destroyed equal (QueensSet 95535 and 8911,
  QueensSet9 766038 and 58100), the outputs byte-equal.  Instructions:
  QueensSet9 18.17 G to 18.55 G (+2.1 percent: the lookup of the escape
  rule at every nested fork of an escaped choice, and the second escape
  set it reads), PermSort 18.517 G to 18.514 G, QueensSet 3.62 G against
  a baseline count that included the background compile of the module
  (6.29 G; the runs after range from 3.62 to 3.75 G).  Peak RSS: QueensSet9
  338 to 346 MB.  CPU seconds vary with the load of the shared machine
  (PermSort 5.5 to 6.9 s on equal instructions).  Against the committed
  Phase 2 record HEAD itself differs (QueensSet 2152979 steps there, the
  O1 pass of this date and the tiered default); the repair moves no
  counter of the three programs: QueensSet meets every choice of its
  argument inside the capsule before the outside decides it, so every
  escape is a fork of the outside as before.
- Tests: test_narrowed_after_start lost its expected-failure mark;
  TestResumedCapsule of unit_setfunctions_bugs.py (SetFunctionsBugs.curry)
  on both backends: the variable inside a list, a choice of the argument
  decided after the start, two capsules over one variable, a nested set
  function whose inner capsule is in the value of the outer and outlives
  it, a set function inside the argument of a set function, and the escape
  of a choice the outside has not decided; test_captured_argument of
  unit_cxx_scheduler.py passes on the C++ backend (the Python backend
  still asserts on a guard of an enclosing set at the root, the known
  failure).  Run on both backends: unit_setfunctions_bugs.py (18 tests),
  unit_cxx_scheduler.py (8), func_setfunctions.py (39), unit_cxx_partial,
  unit_cxx_gc, unit_cxx_counters, unit_cxx_variable, unit_cxx_passthrough,
  unit_cxx_stack, unit_cxx_heap (after the prepare pass of the benchmark
  corpus, whose objects a header change makes stale), unit_py_fairness,
  unit_py_copy, unit_cxx_interp, unit_cxx_gc_counters, func_kiel,
  unit_examples; the two set-function files in the stress mode of the
  collector.  No configuration is written by a split any more, so the
  comments of queue.hpp and configuration.hpp describe the sharing again.
- Example 21: the $## of scenario (d) is no longer a workaround for the
  defect, and the example without it gives the right output on the C++
  backend; but it took 88.7 s against 2.6 s with the operator, and the
  Python backend exceeded 2 GB in scenario (d).  A narrowed variable costs
  inside a capsule: every choice of its generator, and of the generators
  of its parts, escapes when the question needs it, one split and one
  restart each, in every candidate.  Measured on the C++ backend: one type
  takes 1472 steps and 81 forks through the normal form and 91618 steps
  and 14825 forks through the variable; scenario (d) at depth 1 425202
  steps and 14825 forks (0.08 s) against 117757458 steps and 18849188
  forks (22 s), at depth 2 1813436 and 104371 (0.24 s) against 422177995
  and 67730131 (94 s, 444 MB).  The operator stays, with the reason in the
  source and the README.  With the operator the Python backend runs the
  scenarios (a) to (c) in 7 s and then suspends in (d), so the backend set
  of the example in unit_examples.py stays the C++ backend alone.
- Review of the entry, applied.  (1) The repair above left two leaks of
  the same class, both found with probes on both backends.  A captured
  choice decided after the start (`let x = A ? B in let vs = valuesOf
  (set2 pickOrCall A (constT x)) in (head vs =:= A) &> (x, vs)` with
  `pickOrCall z f = z ? f 0`) gave (A, [A, A]) and (B, [A, A]): the choice
  is in no escape set, so the nested fork pruned it by the fingerprint of
  the alternative that ran the capsule first, inside the capsule both
  alternatives share, and update_fp writes nothing into the nested
  fingerprint when the walk decided.  The same with a captured variable
  ([A, A] twice) and with the captured occurrence before the guarded one
  (`pickOrBoth z f y = [z] ? [f 0, y]`: (B, [[A], [A, B]]), the captured
  element from the A world).  And a guarded shape two levels deep: an
  outer capsule alive and shared whose configuration consumes the second
  value of an inner capsule over the variable after the outside narrowed
  it (`inner3 y = let ws = valuesOf (set2 pickOrCase A y) in head ws ?
  (ws !! 1)`, `set1 inner3 u`) gave [A, A] twice: the escape from the
  inner capsule reached the configuration of the outer one, and the fast
  path took the side that read_fp found in the outside through the walk,
  into the private spine of a configuration that both alternatives of the
  outside share.  Two rules close them, on both backends.  choice_escapes
  escapes a choice in no escape set as well when an enclosing
  configuration has decided it (the walk over the enclosing fronts, the
  fingerprint of the nested configuration itself unread); a choice no
  enclosing configuration decided is encapsulated as before.  And the fast
  path of allValues_step and allValues takes the side at once only when
  the configuration owns the decision (RuntimeState::owns_decision,
  owns_decision): it made the choice itself, or its queue was split on the
  choice (Queue::decided).  A side that an enclosing configuration alone
  decided is not taken: the choice node stays, reaches the root of the
  configuration, and escapes in turn, up to the configuration that decided
  it, so every capsule between them is split and each world runs its own
  side.  The invariant that results: the walk of fork and read_fp prunes
  by an enclosing decision only in a queue that an escape of the choice
  has bound to that side.  The call-time reading of the captured shapes
  holds (choiceCaptured, goal10 and goal11 of applyS.curry), and the
  creation-time copy of holds_private_state stays: for a captured choice
  decided before the start it is the cheaper path, one capsule per
  alternative instead of a split and a restart per choice.  Tests in
  TestResumedCapsule: the captured choice, the captured variable, the
  captured occurrence before the guarded one, and the outer capsule alive
  over the variable and over a choice.  (2) The enclosing-set escape rule
  had its only test in a class the Python backend skips; nestedCaptureCase
  of CxxScheduler.curry (the capture under a case, so the guard of the
  outer set is below the root) is the shape both backends run, in
  TestSetGuardRoot.  (3) The comment of fork said the walk reads nothing
  for an escape-set choice; it reads it after the split, which the split
  relies on: reworded.  (4) The for header of choice_escapes was over 79
  columns: wrapped.  (5) The factor in Overloads.curry (275) did not match
  the measured steps (117757458 against 425202, 277): the comment says
  about 280 and quotes the counts.  (6) rts.hpp declared filter_queue
  without a definition since the Python twin became split_queue: removed.
- Measured after the review (C++ backend, one repetition, the same
  machine, after the prepare pass of the runner, which compiled 1236
  modules in 12 directories after the header change): QueensSet 2152241
  steps, 511428 forks, 3 collections; QueensSet9 12146105, 2979683, 11;
  PermSort 12531076, 1588167, 9: equal to the HEAD build's, and the
  outputs byte-equal to the HEAD build's; against the committed Phase 2
  record the counters stay within the tolerance, as HEAD's do (that record
  has no instructions).  Instructions: QueensSet 3.64 G (the HEAD record's
  6.29 G counted the background compile); QueensSet9 18.67 G against
  18.17 G at HEAD, +2.8 percent, of which 0.7 is the review's walk over
  the enclosing fingerprints at every nested fork of an encapsulated
  choice (18.55 G before the review); PermSort 18.52 G against 18.52 G.
  Peak RSS of QueensSet9 346 MB (HEAD 338).  Tests: unit_setfunctions_bugs
  22 tests per backend (4 new), unit_cxx_scheduler 9 (1 new),
  func_setfunctions 39, the two unit files in the stress mode of the
  collector, unit_examples 20 per backend, and the unit suite through the
  runner on both backends at width 2: 160 file runs, 1949 tests, one
  failure, test_imports of unit_curry2icurry.py on the Python backend
  (the interface of an import missing from a fresh copy), which passed
  on the C++ backend in the same run and alone on the Python backend
  afterwards: a test of the toolchain that depends on the order of the
  files, not on the runtime.
- Left: the suspension of the Python backend in scenario (d) of example
  21 with the operator (a residual of $## inside the set function over
  anyType; the simple shapes, forced and forcedStrict, pass); the cost of
  a narrowed variable inside a capsule (above): the escape of each
  generator choice could prune in place when the enclosing configuration
  decided it and the capsule is private, but the runtime cannot tell a
  private capsule from a shared one; the walk of holds_private_state could
  skip the guarded subtrees; the escape rule's lookup costs QueensSet9 two
  percent of its instructions, and the rule of the review adds a walk over
  the enclosing fingerprints at every nested fork of an encapsulated
  choice (0.7 percent more, above).

2026-10-05: rotation by a ticker (K1, #68) and the deterministic mode (K2, #69)
- The C++ backend rotates its queue of alternatives in one of two modes,
  selected by the interpreter flag ``rotation`` (flags.py) and by the
  environment variable SPRITE_ROTATION, which sets the flag below
  SPRITE_INTERPRETER_FLAGS (whose items now split at their first colon, so
  rotation:time:10ms is one item).  The forms are time:<N><unit> (s, ms,
  us, ns; a decimal is fine) and steps:<N>; both numbers lie between 1 and
  2**63, so that the runtime can hold them (a larger step count failed at
  the first evaluation with a TypeError of pybind11 before the review);
  other text is a ValueError when the interpreter is made
  (flags.parse_rotation, flags.check_flags in Interpreter.__new__), so a
  typo in the variable fails the tool at once with the sentence.  An empty
  SPRITE_ROTATION counts as unset everywhere: the interpreter takes its
  default, and cytest, the runner (cli.environment), the harness
  (Settings.environment) and run-tests.sh set step mode over it (they kept
  an empty value before the review, so SPRITE_ROTATION= ran the tests in
  time mode without a word).  Default: time:10ms for the tools
  (sprite-exec, the REPL, curry.eval, python -m curry).  Step mode,
  steps:65536, is set through the environment by the test runner (so
  SPRITE_ROTATION=time:10ms runs a file in time mode), by cytest before
  the first import of curry (a file run by hand with unittest discover is
  in step mode too), by the benchmark harness (-e SPRITE_ROTATION=time:10ms
  measures time mode), by run-tests.sh (ci.yml's test script) and by the
  env block of perf.yml.  The Python backend keeps its step budget and
  ignores the flag; the docs say so.
- The design as built (cyrt/ticker.hpp, ticker.cpp).  One atomic byte
  g_tick.  A detached thread named cyrt-ticker sets it every quantum with
  a relaxed store; its state (mutex, condition variable, the count of the
  evaluations in time mode on the C stack, the started flag, the quantum,
  the tick count) is a leaked singleton, for the reason the B3 entry
  records (the destructor of a condition variable blocks until its waiters
  left, and a static one hung the exit).  The thread touches g_tick and
  that state alone and ends with the process.  RuntimeState carries the
  cadence (rotation_steps, 0 for time mode; rotation_next;
  rotation_quantum_ns), passed from the flags through the RuntimeStateBase
  constructor like stack_limit; the constructor refuses time mode with a
  quantum of zero (std::invalid_argument, a ValueError in Python), and so
  does ticker_enter, because a thread with no quantum would never release
  its mutex (the Python side always passes at least one nanosecond; the
  guard is for the constructor itself).  check_interrupts: stepcount
  increments in both modes (the concurrent conjunction reads it as its
  progress marker, constr.cpp); in step mode the check is stepcount ==
  rotation_next, then rotation_next += rotation_steps, which reproduces
  the cadence of the mask of today exactly and admits any count; in time
  mode it is a relaxed load of g_tick, cleared when set; the slow path is
  today's: the pending swaps of tiered execution, then the scan of qstack
  for the outermost queue with more than one configuration (E_ROTATE); the
  collection check follows as before.  The outermost procD of a state
  holds a TickerScope in time mode: ticker_enter counts the evaluation and
  starts the thread when the state has none (under the mutex;
  pthread_atfork is registered there once) or wakes it when it is parked;
  a nested procD (a set function) is inside the scope; the count drops at
  exit without a lock.  The thread parks (cv.wait) when it finds the count
  zero at a tick, so an idle host has no wakeups; it sets the tick only
  while the count is positive.  A notify happens only when the thread is
  parked, so a goal with many values costs one uncontended lock per value
  and no context switch of the thread.  When std::thread throws (EAGAIN
  under RLIMIT_NPROC, ENOMEM), ticker_enter takes the count back and
  throws a std::runtime_error that names the ticker and step mode
  ("cannot start the ticker thread of time mode (...); set the rotation to
  step mode, for example SPRITE_ROTATION=steps:65536"), so the next
  evaluation tries again; before the review the count stayed at one, and
  every later evaluation of the process ran without a thread and without
  a rotation after one bare "Resource temporarily unavailable".  Fork:
  the child handler of pthread_atfork gives the child a fresh state (no
  thread, the count of the evaluations in flight carried over, the byte
  clear); the first evaluation entered in the child starts a ticker of
  its own whatever the count, because the state has none (before the
  review only a count of zero started it, so an evaluation entered while
  one carried over from the fork was still in flight, a Python callback
  inside a step that evaluates, ran without a ticker); an evaluation in
  flight at the fork runs in the child without a ticker until then.  A
  host that evaluates in time mode is multi-threaded, so Python warns on a
  later os.fork; the docs say so, and that step mode starts no thread.
  The hook the native workers of family W poll: ticker_due() (and
  ticker_clear()).  The bindings gained ticker_status() (started, parked,
  active, ticks, quantum_ns) and, for the tests, ticker_enter(quantum_ns)
  and ticker_leave(), which do what the outermost scheduler does on entry
  and exit.  The host-interrupt check at the safepoint (open question 7 of
  the plan) is not in: an unwind from the safepoint through generated code
  and the nested schedulers needs its own design.
- Gate K, measured on this machine after the review (12 cores, other
  users' processes, the C++ backend, interpret:off, three repetitions,
  the deterministic columns decide; the tables with the commands are in
  the notebook, perf-program/k-measurement.md):
  the harness, instructions of time mode over step mode: Tak1 16242.9 M
  over 16174.8 M (+0.42 percent), Queens10 18109.8 M over 18019.5 M
  (+0.50), PermSort 18592.9 M over 18557.9 M (+0.19); compare
  --deterministic says "same" for all three with the counters equal (Tak1
  33629915 steps, Queens10 44630642, PermSort 12531076 steps and 1588167
  forks, in both modes), and the CPU seconds of this pair agree within
  three percent (~cpu 0.99, 1.03, 1.00; the first pair, measured during a
  load of the machine, had shown 1.33 to 1.50).  Against the HEAD build
  (the install of the branch at 4e21df99, its own compiled objects of the
  three programs, perf stat -e instructions:u on sprite-exec -t --stats,
  three repetitions each, the modes alternating): HEAD 16071.0 M, step
  mode 16172.0 M (+0.63 percent), time mode 16239.3 M (+1.05) on Tak1;
  17882.9, 18015.6 (+0.74), 18105.5 (+1.24) on Queens10; 18516.7, 18554.5
  (+0.20), 18588.5 (+0.39) on PermSort; the counters equal in all three.
  Per completed step that is 3.0 instructions more in step mode and 5.0
  more in time mode (5.7 on PermSort, whose 500 ticks each scan a long
  qstack).  So time mode is not cheaper than HEAD: the design's "a load
  and a branch replace an increment, a mask, and a branch" did not come
  true, because the increment of stepcount stays for the conjunction and
  the test of the mode was added; the time-mode path is HEAD's increment,
  then a load of rotation_steps, a test and a branch, then a load of the
  address of g_tick through the GOT, a load of the byte, a test and a
  branch, then the collection check (objdump of the staged libcyrt.so).
  The CPU seconds show none of it: the nine perf stat runs per program
  lie within three percent of each other with no order between HEAD and
  the two modes (Tak1 1.23 to 1.31 s, Queens10 1.38 to 1.47 s, PermSort
  4.98 to 5.27 s); the ticker shows as about 100 context switches per
  second (129 to 148 on the 1.3 s programs, 517 to 547 on PermSort).  Two
  ways to take instructions back were left for a later pass: a hidden
  alias of g_tick for the hot path inside libcyrt (the exported byte stays
  for the native workers and the bindings, which include the header), one
  load per step; and the progress marker of the conjunction on
  steps_total, which would free time mode of the increment, three loads
  and stores per step, but changes when a conjunction suspends after a
  step inside hnf.  Latency of fair (loop 0 ? 42) in time mode, 20 probes
  with the thread parked before each: min 10.6, median 10.6, max 16.4 ms;
  in a child of os.fork, 10 probes: 10.6, 10.7, 16.4 ms; all within three
  quanta (step mode takes 4 ms for its 65536 steps).  Idle: after an
  evaluation the thread parks within a quantum, and over two seconds its
  ticks and its context switches do not move (the test reads
  /proc/self/task).  A goal with 40320 values (perm [1..8]) costs the same
  wall and CPU seconds in both modes (0.14 to 0.15 s, the first
  measurement); the extra voluntary context switches are the ticks.
- Tests.  unit_cxx_rotation.py (20 tests; the Python backend runs the
  setting tests and skips the rest with the reason): the setting parses
  (both forms, the units, the bounds, the errors), the default is time
  mode and a child without the variable gets it, an empty variable counts
  as unset for the interpreter and for cytest, the precedence of
  SPRITE_ROTATION, SPRITE_INTERPRETER_FLAGS and the flags of a reload, a
  bad value is an error when the interpreter is made and fails the import
  of curry in a child with the sentence; step mode gives the counters of
  today (countQueens 8 of CxxGc: 2180142 steps and 511409 forks; psort 8:
  17093 and 1636; measured on the build before the change), repeats them,
  never starts the thread, and rotates after 65536 or 1000 steps as set;
  time mode yields 42 for fair of CxxRewrite within a generous bound (the
  test says why; the measured latency is above), the thread parks after
  an evaluation and a parked thread moves neither its ticks nor its
  context switches (voluntary and involuntary, from /proc/self/task) over
  two seconds (the count is taken once two reads a few milliseconds apart
  agree, because the thread shows parked before its wait blocks), a child
  of os.fork starts a ticker of its own and yields fair's value (the
  forked child sets an alarm so a regression cannot leave a spinning
  process behind the timeout), a child forked while an evaluation is in
  flight (ticker_enter in the parent) starts the ticker at its first
  evaluation although the carried-over count is positive, a thread that
  cannot start (RLIMIT_NPROC set to one after the imports, the modules
  interpreted so no compile forks; the test skips where the limit is not
  enforced) fails the evaluation with the sentence twice over and leaves
  the count at zero and step mode working, a zero quantum is a ValueError
  of the constructor and of ticker_enter, and a deterministic evaluation
  takes the same steps in both modes; and the rotation tests of
  unit_cxx_rewrite.py and unit_cxx_stack.py run in a child under each mode
  (8 tests per mode).  The two tests that pin the cadence force step mode
  themselves: test_rotation_inside_the_constraint of unit_cxx_scheduler.py
  and test_writeFile_keeps_prefix_across_rotation of unit_io_bind.py;
  test_split_suite of unit_benchmarks.py runs its harness in step mode,
  because its rows compare the counters of QueensSet, which differ between
  the measured repetition and the one under perf in time mode (the record
  then holds None, and the test failed under SPRITE_ROTATION=time:10ms
  until the review).  unit_runner.py and unit_benchmarks.py check the
  variable in the child environments, the empty value included.  Runs
  after the review: unit_cxx_rotation.py alone, 20 tests ok; the six
  files unit_cxx_rotation, unit_cxx_rewrite, unit_cxx_stack,
  unit_cxx_scheduler, unit_io_bind and unit_benchmarks through the runner
  under SPRITE_ROTATION=steps:65536 and under time:10ms, 6 of 6 each (140
  tests); the unit suite on the C++ backend, 81 of 81 files, 0 failed,
  wall 122.2 s; the fast tier on the Python backend (./run_tests --fast
  --backend py), 65 of 65 files, 0 failed, wall 48.7 s.  Docs:
  EnvironmentVariables.rst (SPRITE_ROTATION and the flag, the thread and
  os.fork, the empty value), sprite-exec.rst, REPL.rst, tests/README
  sections 4, 9 and 10, the harness README, AGENTS.md, flags.py.
- Left: the host-interrupt check (Ctrl-C into a long C++ evaluation,
  open question 7); a manifest entry for unit_cxx_rotation.py (the
  runner assumes the median duration); a sprite-exec option for the mode
  (the flag and the variable serve today); the quantum is one per process
  (the last evaluation that entered sets it; two interpreters with
  different quanta share the thread); the instructions per step above
  HEAD (the two options above); a newer baseline of the nightly set with
  the instructions column (the Phase 2 record has none, and its Queens10
  and PermSort counts predate the O1 pass and the #61 repair).

2026-10-06: the C++ backend is the default, the compiler-free notice, the nightly interpreter jobs (B4, #82, stages 1 to 4)
- The cheap bundle of the audit (C1 to C3).  cytest.skipIfInterpreted
  skips a test under interpret:new and interpret:all on the C++ backend; a
  module loaded in those modes has no compiled object.  It marks the
  tests that read one: four tests of TestPlainDataBOM in unit_cxx_bom.py
  (the two Prelude-object tests of TestFunctionNames skip at run time
  instead, when the Prelude is interpreted; see the review below),
  test_literal_count and test_generated_code of unit_cxx_literals.py,
  test_generated_code of unit_cxx_partial.py and of unit_cxx_rewrite.py,
  TestGeneratedCode of unit_cxx_passthrough.py, and testGetICurry and
  testGetImpl of unit_inspect.py.  test_advisory_pass of unit_runner.py
  strips SPRITE_INTERPRETER_FLAGS from the environment of its stub pass
  (the job read interpret:all from it and expected a JSON product).
  tests/README section 4 names the right sets now: the files that pin
  interpret:off for themselves pass under every mode (unit_cxx_toolchain,
  unit_make, and single tests of unit_api, unit_compile_modules,
  unit_findcurry, unit_optimize, unit_prebuild; unit_optimize_applies
  pins new for one test and skips one under all); the entry of
  2026-10-04 on B2 above carries a correction note.
- Stage 3, the default.  configure --with-default-backend {py,cxx}
  (DEFAULT_BACKEND = 'cxx' beside DEFAULT_JOBS) writes DEFAULT_BACKEND
  of Make.config, which reaches sysconfig/default_backend and the default
  of the flag backend.  The test runner falls back to the installed
  default (cli.installed_backend reads the sysconfig file; the constant
  DEFAULT_BACKEND of the package is the last resort), and its --backend
  help says so.  sprite-exec -b and python -m curry read the default from
  the flags already and show [default: cxx].  The REPL gained the option
  backend of :set: the listing shows the backend of the session, and
  :set backend NAME reloads the interpreter and loads the module again
  (REPL.switch_backend; :load records its argument).  Docs: README,
  Quickstart, sprite-exec, EnvironmentVariables (with the interpret
  flag), MajorFeatures (the LLVM backend text replaced by the C++
  backend), CompilationPipeline, REPL, the READMEs and run scripts of
  examples 03 and 04 (they select backend:py and say why), AGENTS.md,
  conda/README.md, and the recipe: build.sh passes
  --with-default-backend=cxx, and the tests run the default backend
  without a flag (the value alone: a short program under tiered ends
  interpreted), the compiler of the environment through sprite-make --so
  (the object and the stamp), and the Python backend by flag (its .py
  product).  The CI matrix keeps the py shard; run-tests.sh names the
  backend of every shard, so the flip changed nothing there.
- Found on the way.  Interpreter(flags) starts from the defaults, not
  from the environment, so the fresh interpreters of unit_py_conversions.py,
  unit_py_interp.py and unit_py_runtime.py had run on the Python backend
  whatever the backend of the session, and ran on the C++ backend after
  the flip: a Node compared with True raises TypeError in the bindings
  (no __ne__ for a Python value), Prelude._Failure is a symbol of the
  Python backend alone, and a module of a known name imported from a new
  source by a new interpreter kept the old code.  The three files name
  backend:py for their fresh interpreters now.  No other test, and
  nothing under src/python but sprite-make, which merges the environment
  flags, builds a fresh interpreter.
- Stage 2, compiler-free.  The notice: once per process, a WARNING of
  curry.backends.cxx.tiered at the first module that stays interpreted
  because config.cxx_tool() is None, with the setting that silences it.
  Probes with the staged install served under another path (a directory
  of symlinks without tools/cxx; SPRITE_DISABLE_SYSLIB_CHECKS=1, because
  the system-library check refuses the symlinked library): under tiered
  the notice named the Prelude and the program gave its value, 1001 in
  1.28 s; the ABI stamp digests the real path of the installation prefix
  (toolchain.object_digest, the repair of C6 of the audit), so every
  library object of a moved installation is stale.  interpret:new failed
  with the CompileError on Prelude.cpp: a stale object is compiled, and
  the mode suits an installation in place alone.  interpret:all ran
  with no message; interpret:off failed with the CompileError that names
  the remedy.  So the notice names the setting that selects the
  interpreter without a compile (tiered._interpreter_setting, after the
  review): interpret:new where the Prelude loaded from its object and no
  current generated file lies beside the module, because that setting
  keeps the compiled library (the Prelude's bytecode costs 0.3 to 0.55 s
  per process otherwise), and interpret:all, which never compiles, where
  the Prelude is interpreted or a current .cpp lies beside the module,
  since new compiles such a module.  The docs page
  Installation/WithoutCompiler.rst says the same.  conda/README.md
  records the split of the package as a
  follow-up of #1, not done here, and the relocation fact: an
  environment whose prefix differs from the build prefix finds the
  shipped library objects stale.
- Stage 4, the nightly.  The interpreter job of ci.yml runs on the
  schedule and on dispatch, one job per mode ('all' and 'off', quoted:
  a bare off is a Boolean in YAML), the whole unit suite and the
  functional suites each; the cache keys carry the mode.  The month of
  the removal gate starts with the first nightly run after the merge.
  The pull-request jobs are unchanged.
- Tests: unit_configure.py (the help, the default, both values, the
  refusals, the sysconfig var, the staged value), unit_runner.py
  (installed_backend on a stub installation, the listing with and
  without the flag), unit_cxx_tiered.py TestNoCompiler (cxx_tool patched
  to None: two fresh modules interpreted, no object, one notice naming
  the first; no second notice; none under new and all), unit_goals.py
  test_repl_set_backend (the listing before and after the switch, the
  value on the other backend, the refusal), unit_conda.py (the recipe
  tests), unit_perf_history.py test_interpreter_job (the nightly jobs).
- Review of the diff: nine findings, all applied.  run_tests --help: the
  epilog names the default backend of the installation beside
  SPRITE_INTERPRETER_FLAGS, as the --backend help does.  The configure
  page (Installation/ConfiguringSprite.rst) documents
  --with-default-backend under "Build options": Make.config,
  sysconfig/default_backend, and the flag that selects the other backend
  for a run; the option table stays a table of the external programs.
  The profiling section of the sprite-exec page selects the Python
  backend (-b py) and says why: a probe of -p on the C++ backend showed
  5335 calls led by the next of the bindings and no step function; on
  the Python backend, 2.2 million calls led by fairscheme.S.
  tests/README and this entry drop unit_optimize_applies.py from the
  files that pin off (it pins new for one test and skips one under all),
  and the correction note is under 2026-10-04.  The notice says "add
  interpret:NAME to SPRITE_INTERPRETER_FLAGS", so the other flags of the
  variable stay, and names the setting by the rule above
  (tiered._interpreter_setting); test_notice_setting pins both branches,
  and the docs page quotes the new text.  :set backend records the
  backend after the reload and before the load of the module
  (REPL.switch_backend; the load is commands.load, a local call, so the
  :set in flight keeps its command and arguments); when that load
  fails, the Prelude is loaded for the prompt, the argument of the :load
  stays for a switch back, and the error is reported (REPL.rst says so;
  TestReplSession of unit_goals.py stubs curry.reload and tests the
  state).  The two Prelude-object tests of unit_cxx_bom.py skip at run
  time when the metadata of the Prelude has no cxx.shlib, so they run
  under new, where the Prelude loads compiled (unit_cxx_bom.py: 0 skips
  under tiered, 4 under new, 6 under all).  The three over-long lines of
  the rewrapped paragraphs are wrapped at the width of their files.
- Results on this machine (another stream at -j 2 beside the runs;
  the install rebuilt by configure and make stage, 78 s):
  SPRITE_INTERPRETER_FLAGS=backend:cxx,interpret:all ./run_tests -j 2
  --mem 4 'unit_*.py': 81 of 81 run, 0 failed, wall 313.8 s (the first
  run found the five files above and the stale workflow test; 0 after
  the fixes); interpret:new on the eleven files that read objects or
  build interpreters: 11 of 11, 0 failed, 18.5 s; the unit suite on
  both backends with --prepare (unit_examples.py included): 162 of 162
  run, 0 failed, wall 443.5 s; the functional suites on both backends
  against the oracle: 42 of 42 run, 0 failed, wall 80.5 s; make -C docs
  html: finished, the new page rendered, and every warning predates this
  entry (the docstrings of stats and materialize, the duplicate object
  descriptions, the two-line term at the scheduler counters of the
  sprite-exec page, the target "fair scheme", _static).  After the
  review (configure and make stage again, 44 s): the unit suite on both
  backends with --prepare, 162 of 162 run, 0 failed, wall 388.0 s; the
  unit suite on cxx under interpret:all, 81 of 81 run, 0 failed, wall
  324.8 s; the functional suites on both backends against the oracle,
  42 of 42 run, 0 failed, wall 70.2 s; the single files unit_cxx_tiered.py
  (19 tests), unit_runner.py (65), unit_goals.py (48 on each backend),
  unit_cxx_bom.py (10 under tiered, new and all); make -C docs html
  finished with the same 142 warnings, and the four edited pages render;
  sprite-exec --stats and python -m curry without -b on a fresh module
  print 1001 through the C++ backend (products .json.z, no .py, no .so);
  ci.yml parses with the interpreter matrix all and off.
- Left.  Stage 4 of the issue, the py shard to the nightly, after a
  month of green nightly runs under all and off, so that the default
  backend keeps a pull-request job at every moment; stage 5, the hidden
  flag for the Python backend, and the decision on curry.save and
  curry.load (a C++ form, or retired with the backend: example 04 and
  three doc pages follow) and on the stepper (cytest.step); stage 6 at
  the gate, #62 and the py-only expected failures.  Also: a Node
  compared with a Python value raises TypeError in the C++ bindings
  where the Python backend answers False; the manifest has no rows for
  the new tests; the conda recipe of this text has not been built; the
  notice under SPRITE_LOG_LEVEL=ERROR is silent, as every WARNING is.
2026-10-06: the binding optimization rewrites the FlatCurry file (#85)
- Cause.  Since 1b554fd1 the build route applied the pass in memory
  (translate_file(..., bindingopt=True)) while the icurry route and the
  oracle translated the file as the front end wrote it: the two routes
  wrote different .icy files for a module with a required Boolean
  equality, and the on-disk oracle check accepted a product of the build
  route only through a second translation (accept_build_route, 4e21df99).
  PAKCS applies the optimization to the FlatCurry file itself:
  preprocessFcyFile in compiler.pl runs BindingOpt, which writes M.fcy_OPT
  and renames it over M.fcy when an equality was replaced.
- Fix.  A FlatCurry writer in the format of the front end, the show of
  Haskell (terms.showhaskell; flatcurry.show and flatcurry.write).  The
  PAKCS writer showterm does not serve: the front end writes Prog "M" ...
  without the outer parentheses, "" for the empty string, the ASCII names
  \NUL to \DEL and \SO\&H in a string, and 1.0e7 for a float.  The writer
  round-trips every .fcy of the corpus byte for byte, the 1260 files
  under tests/data and the 1210 files of the archive, the library
  included, on its first run; func_flat2icurry.py pins it
  (test_writer_round_trip), and unit_flat2icurry.py tests the escapes,
  the numbers and the parentheses (TestHaskellWriter) and the sample, the
  probes and the library modules of the archive (TestFlatCurryWriter).
  The rewrite step, bindingopt.optimize_file: both routes run it after
  the front end and before the translation (_frontend.curry2icurry; the
  icurry branch of Curry2ICurryConverter.convert runs the front end and
  the rewrite before the icurry program, whose own front-end run finds
  its files current and leaves the rewritten file alone, checked by the
  time of the file).  The file is written again when the pass replaced
  an equality, as transbooleq writes it; a rule whose equalities all stay
  is rebuilt (its $ reduced) only then, so optimize_bindings returns the
  program itself when the count is zero (the Prelude is such a program:
  no replacement, a rebuilt rule).  The translation runs with
  bindingopt=False on both routes, with icurry_compat as before.  The
  pass is idempotent over the archive: a second run replaces nothing and
  gives the same program, and the optimized program survives the writer
  and the reader (test_rewrite_is_idempotent).  The cache digest of the
  icurry route covers the sources of the pass (cache.REWRITE_SOURCES).
- Decisions.  --bindingopt of python -m curry.toolchain.flat2icurry stays
  the in-memory pass: it translates a file the routes have not rewritten
  as the routes would, and leaves the file alone.  Two hand runs:
  python -m curry.toolchain.flat2icurry.rewrite M.fcy... rewrites
  FlatCurry files in place (a module of its own, so python -m gives no
  warning about a module the package imported), and sprite-make
  --rewrite-flat M runs the step from Curry to ICurry again, current or
  not, without the ICurry cache (a hit writes no FlatCurry), and makes
  the ICurry and the later products from the rewritten file; it runs in
  this process (--jobs 1) and needs the source.
- The oracle.  test_products_on_disk runs without the fallback;
  accept_build_route is gone.  Before the change, with the fallback off,
  two of the 1258 pairs on disk in this worktree differed, CxxInterp and
  CxxScheduler: an .icy of the in-memory pass beside the front end's
  .fcy.  No product differed through icurry_compat alone, so no fallback
  for that setting is kept.  The hand run rewrote the two files (one
  equality each), and the check passes.  The pass changes 7 of the 1260
  files under tests/data (CxxInterp, CxxScheduler, atableNoflex,
  sendMoreMoney with 15 equalities, BFSvsPar, RegExp,
  poker_four_of_a_kind) and some files of the archive.
- What stays as it is.  The committed ICurry of the Curry library
  predates the pass and is not regenerated: make stage derives the JSON
  from the committed .icy and never translates a library module unless
  SPRITE_REBUILD_ICY=1; that run would now rewrite the library's .fcy
  under curry/lib/.curry and translate the optimized program.  The
  archive's .fcy files are the front end's and pair with the .icy files
  icurry wrote from them; a rebuilt archive packs rewritten .fcy files
  with icurry's translation of them.  A product written before this
  change keeps its .fcy as the front end wrote it until the source is
  newer (the front end then writes the file again and the route rewrites
  it) or a hand run rewrites it; an .icy of the pre-pass era is remade
  only by sprite-make --rewrite-flat or a newer source.  In this worktree
  the .fcy and the .icy of sendMoreMoney under tests/data are both
  unoptimized (made before the pass) and stay so; the test compiles the
  program from text.  A .fcy that PAKCS rewrote in place (the oracle
  runs PAKCS on the test modules, and its BindingOpt writes the showTerm
  format) is not the front end's text: the reader reads it, the pass
  finds nothing to replace, and the round-trip test leaves such a file
  out; the corpus holds none today.
- Verification, one file per process under prlimit --as 6 GB, both
  backends: unit_flat2icurry.py 77 OK (2 skipped),
  unit_flat2icurry_bindingopt.py 31 OK, unit_curry2icurry.py 15 OK (the
  icurry route is configured in this install, so its test ran: both
  routes write the same bytes for sendMoreMoney, 15 constrEq),
  func_flat2icurry.py 6 OK, unit_py_residuation.py 3 OK.  The probes:
  the writer over the 2470 files of the corpus and the archive, none
  differ; the icurry route on a fresh copy of sendMoreMoney gives the
  bytes of the port and leaves the rewritten .fcy with its time;
  sprite-make --rewrite-flat --json over a module whose .fcy was put
  back to the front end's text rewrites the .fcy and the .icy and writes
  the JSON, and refuses --jobs 2 and a module without a source.
  tests/manifest.json has no entry for unit_flat2icurry_bindingopt.py;
  unit_curry2icurry.py has 15 tests now (11 recorded) and takes about
  25 s more for the test of the icurry route.
- Open.  Pairs of other worktrees and machines made in the in-memory era
  differ in test_products_on_disk until the hand run; the docstring of
  the test names the two commands.  The archive is not rebuilt here, and
  the library is not regenerated here.
- Review follow-ups.  The icurry route needs the front end as well now (it
  runs it first and rewrites its FlatCurry file), so a missing front end
  is reported by Curry2ICurryConverter.command before any program runs,
  with the real requirement, and _frontend.command no longer recommends
  the icurry route in its message.  configure requires the front end
  whatever --with-icurry says (set_frontend; set_curry2icurry_tool aborts
  with the reason when it is left out), and the configure page says so.
  The digest of the icurry route covers the front-end flags too
  (cache.frontend_digest): the file icurry translates is the one Sprite's
  run of the front end wrote with them (icurry finds it current), so a
  change of CURRY_FRONTEND_FLAGS changed the output of the route under an
  unchanged key.  Tests: test_tool_selection of unit_curry2icurry.py (both
  messages, the front end mocked away), test_front_end_required_with_icurry
  of unit_configure.py, and test_route_is_part_of_the_key of unit_cache.py
  (the icurry branch when icurry is configured).

2026-10-06: potholes (#63, #86, example 21 on the Python backend, #62)
- #63, a point-free definition applied to arguments from Python.  Cause:
  the untyped builder (ExpressionBuilder in expressions.py, behind raw_expr
  and expr with typed_expr off) counted the arguments against the arity
  alone: fewer gave a partial application, more went to make_node as they
  were, where the Python backend raised TypeError and the C++ binding
  dropped the surplus in silence (Node_create handed the vector to
  Node::create, which packs by the format string and never counts).  The
  typed builder of Y7 already folds apply over the surplus, so
  curry.eval(M.norm, 3) and curry.expr gave 7 on both backends before the
  change; raw_expr gave TypeError on the Python backend and the partial
  application norm on the C++ backend.  Fix: the untyped builder applies
  the head to its arity and folds Prelude.apply over the rest, one node
  per argument, the target on the last, as the typed builder does; a
  symbol that is not a function applied to a surplus raises
  CurryTypeError with the sentence of the typed builder, without the
  scheme ("Just takes 1 argument, 2 given", where the typed builder says
  "Just :: a -> Maybe a takes 1 argument, 2 given"; the review found the
  two builders with two sentences for one mistake); Node_create checks the
  count against the arity, equal or
  below it for a partial application, and raises TypeError with the
  message of the Python backend; create_partial_applic of the Python
  backend runs the count check before its assertion, so a partial
  application with too many arguments raises TypeError on both backends.
  Tests: TestSurplusArguments of unit_expr.py on both backends (norm =
  (+ 1) . (* 2) through raw_expr, expr and eval; twice applied once and
  twice; a constructor with a surplus through both builders; make_node
  with two, zero and one too many arguments).  The docstring of raw_expr
  names the rule.
- #86, a name produced by a functional pattern stays narrowed.  Measured
  with sprite-exec --stats on the C++ backend, step mode, a module with
  source (stem ++ ".o") = stem ++ ".c", the same read through a set
  function (sourceS t = case sortValues (set1 source t) of [s] -> s), and
  the same with the result evaluated to normal form (sourceN t = id $##
  source t).  One comparison: 151, 148 and 168 steps, 8 forks each.  One
  name compared 200 times (the name shared, the comparisons not): 222141
  steps and 1202 forks through source, 14723 steps and 8 forks through
  sourceS, 180362 steps and 1202 forks through sourceN.  nub over 49
  names: 944283 steps and 9800 forks, 28308 and 392, 1048653 and 14210.
  The list of distinct names read twice: 1889471 and 19796, 29094 and
  392, 1705647 and 28616.  The Python backend forks the same (1202
  against 8) and takes 2.36 s against 0.27 s for the 200 comparisons; it
  counts a rewrite alone (15484 against 15503 steps).  Cause: a binding
  and a narrowing are private to the configuration that made them, so a
  read of a bound variable, or of a narrowed one, copies the spine of the
  configuration from its root to the variable with the binding or the
  decided generator in the copy and restarts the evaluation from the root
  (replace_freevar and Scan::copy_spine of the C++ runtime; N and hnf of
  fairscheme.py).  The restart repeats the steps of every function on the
  spine, and procS counts each (the inflated steps); a decided generator
  choice met again is pull-tabbed and forked to its one side (the
  forks).  Nothing of the copy goes back into the shared graph: the next
  reader of the shared name starts over.  The pattern variables of a
  functional pattern are exactly such variables: the front end writes
  source x = cond ((stem ++ ".o") =:<= x) (stem ++ ".c") with stem free,
  =:<= narrows stem by its generator and binds each character, and the
  result holds them.  $## does not help, because its normal form lives in
  the private spine as well.  A set function helps, because the capsule
  has its own queue and root: the spine copies stay inside it, its
  configuration reaches a value in which every variable is resolved, and
  allValues_step writes that value in place as plain data.  Options and
  cost.  (1) Write the binding into the shared graph when the
  configuration owns the node: unsound without an ownership test, and an
  epoch stamp per node does not give one, because an in-place rewrite of a
  shared redex publishes a young node to every configuration.  (2) A
  per-configuration map from a copied spine node to its private copy,
  consulted at every descent, so a second reference to a copied node
  within one configuration finds the copy: restores sharing and removes
  the repeated normalization, but not the copy per read; a flag bit per
  node to make the test cheap, copy-on-write of the map at a fork, and
  the collector's view of the map: several days.  (3) A ground copy of a
  rule's result once a functional pattern matched: a change of
  strictness (the result may hold an infinite or undefined part) and the
  copy would still be private to the configuration.  (4) The idiom: read
  the result through a set function.  Taken: (4), with the measured
  reason in the primer (CurryPrimer/Syntax.rst, the note under functional
  patterns), in section 5 of the README of example 24, and in
  tests/unit_funcpat_narrowed.py, both backends: the same values by every
  route, the forks of the direct route and of the $## route growing with
  the comparisons, those of the set-function route staying at the match,
  and on the C++ backend the steps of the direct route above five times
  the steps of the set-function route.  A runtime change that removes the
  cost flips the assertions on the direct route, which is the point of
  the test.  Open: option (2) is the runtime work if the cost matters
  beyond the idiom.  (5) The review proposed the shortcut of the #61 fix
  (holds_private_state): when the outermost queue holds one configuration
  and no queue encloses it, write the binding, the generator and the
  chosen side of a decided choice in place instead of copy_spine and a
  restart.  Measured on the Python backend with the evaluator patched in
  a probe (N and hnf of fairscheme.py: the write goes to the slot of the
  parent at the cursor, under the condition and with no set guard on the
  path; the tree is unchanged), module of unit_funcpat_narrowed.py: many
  through source 15483 steps, 1202 forks, 2.22 s before; 15483 steps, 7
  forks, 0.52 s with the shortcut; 15502 steps, 8 forks, 0.13 s through
  sourceS.  distinct: 324 forks, 0.09 s before; 62 forks, 0.05 s with
  the shortcut; 72 forks, 0.02 s through the set function.  The values
  are unchanged.  The forks go, but the binding is still resolved at
  every read: 1201 writes for the 200 comparisons, because hnf meets the
  bound character at the slot of the redex (eqChar x 'a'), which is
  private to the comparison, while the holder of x, the cons cell of the
  name, keeps the variable; the configuration owns the binding, not the
  node.  Forwarding the variable node and the decided choice node
  themselves (a second variant of the probe) brings the writes down to
  12 and leaves the time at 0.54 s: the rest is the non-strict
  constraints that =:<= left in the data, lifted to the root (a spine
  copy) at every read; the set function leaves plain data.  Besides, a
  forwarded variable breaks the invariant that the variable table maps a
  vid to a free variable node (get_freevar, is_void, constrain_equal),
  and every in-place publication of configuration state is visible to a
  later evaluation of a shared subexpression (the #61 shortcut shares
  that property).  So (5) removes the forks and four fifths of the time
  on the Python backend and stops short of the set-function route; the
  C++ runtime (replace_freevar and the choice path of hnf) was not
  changed, and the idiom stays.
- Example 21, scenario (d) on the Python backend.  Cause: apply_gnf, the
  step of $## (backends/py/currylib/prelude/apply.py), checked for
  residuals before the normalization ran: _applyspecial is a generator,
  and its body, the normalization, ran only when Node consumed it after
  the check; the check walked the argument as it stood, through the
  logical successors of a Variable, which include the generator of a free
  variable, and counted every void variable it met.  In scenario (d) the
  argument was the narrowed type variable t, and the walk went through
  its generator into the Class branch this alternative had not taken,
  down to the generator of the name string and the tail variable of its
  cons branch, which no alternative ever bound (the path, printed from the
  suspended capsule: COMPLEXCASE0[0] / _Free[1] / _Choice[2] x3 /
  Class[1] / _Free[1] / _Choice[1] / :[1]); every configuration of the
  capsule suspended on that variable, and ready() raised
  EvaluationSuspended out of the nested queue.  Fix: the step runs the
  normalization first (the list of the generator), then walks the normal
  form without entering a generator and suspends on the free variables
  without a generator, the rule of applygnf_step in the C++ runtime.
  Result: depth 1 gives "60 types; after the lvalue transformations, 12:
  ..." on the Python backend; ./run with backend:py takes 24.7 s and
  matches expected.out; the backend set of the example in
  unit_examples.py is {'py', 'cxx'}, and sections 3 and 5 of its README
  say so.  Test: TestNormalFormOfNarrowedVariable of
  unit_py_residuation.py on both backends ($## over a variable narrowed
  to a value with a String field, alone and inside a set function, and
  the free-name shape that gives its value and then suspends on both
  backends, as the C++ backend did before).  The review doubted that the
  reduced shapes pin the defect, after an earlier claim that they pass
  with the old step too.  Measured again with the committed apply.py
  loaded beside the tree and its apply_gnf installed as the step of $##:
  pick 1, picks and the new pickFirst 1 did not end within 20 s on the
  Python backend, before and after the ready() fix below; with the new
  step each ends at once.  So the shapes do pin the defect.  pickFirst,
  added to NARROWED, reaches the step with the bare narrowed variable, the
  condition of the entry: ($##) comes first, suspends on the free
  variable, and the narrowing in the other conjunct of (&) wakes it.
- ready() of the Python backend read the head of the queue.  _make_ready
  filtered the residuals of a configuration with rts.is_void and no
  configuration, so has_binding and is_narrowed read rts.C, the blocked
  head, for every configuration behind it.  A configuration behind a head
  that held a binding of its residual was released, its step suspended
  again at once, and the two took turns without end:
  ((v =:= 1) &> (u + 1)) ? (v + 1) with u, v free spun at full load
  (timeout at 40 s) where the C++ runtime, whose _make_ready passes the
  configuration, suspends at once.  Fix: is_void gets the configuration.
  Test: TestReleaseReadsTheWaitingConfiguration of unit_py_residuation.py,
  both orders of the alternatives, both backends.
- #62, list functions over about a thousand elements on the Python
  backend.  The recursion is not in hnf alone: a step function, generated
  or built in, calls hnf on its inductive argument from the middle of its
  body, and hnf calls the step of that redex: length [1..2000] ends in
  RecursionError with 16386 frames, eight per element (step_tracer, S,
  the step of plusInt, apply_unboxed, _unbox, hnf_or_free twice, hnf).
  An iterative evaluator would have every step hand its demand back to a
  driver and resume after it, a rewrite of the generated code and of the
  built-in steps: not under a day.  The first decision of this entry
  left the limit of 16384 frames and documented it, with the reason that
  the chain re-enters C at every step and the C stack bounds it.  The
  review measured otherwise, and the measurement was repeated: with
  generate_values under a limit of ten million frames, on the main thread
  with its 8 MB stack and under prlimit --as 2 GB, length over 10000
  elements passes in 0.6 s (76 MB peak), 40000 in 2.8 s (175 MB), 160000
  in 10.8 s (575 MB, 1.28 million frames); sort over 10000 passes in
  8.5 s under 131072 frames already; the C stack bounded none of them.
  The depth needed is the eight frames per element: 10000 pass and 16000
  fail under 131072; 20000 pass and 32000 fail under 262144.  Decision
  taken instead: the flag recursion_limit of the Python backend, the
  recursion limit of the interpreter while a value is computed
  (generate_values), 262144 by default (about thirty thousand elements
  under length, 175 MB at the limit, about 4 KB per nested element), an
  int or None (the limit of the interpreter stays); the C++ backend
  ignores it, as the Python backend ignores stack_limit.  An alternative
  that reaches the limit is handled as before (overflow rotation and the
  drop of a stuck one).  unit_py_fairness.py sets 1 << 14 for its tests,
  so its lists of ten thousand elements still overflow in about a second
  (6 s for the file, as before).  tests/unit_long_lists.py now passes
  length and sort over ten thousand elements on both backends (py 6.3 s,
  the sort is 5 s of it) and pins the flag: under 1 << 14 the same length
  raises RecursionError and a thousand elements pass.  Docs: the flag in
  interpreter/flags.py, the sprite-exec page (Selecting a Backend and
  Bounding One Alternative) and the EnvironmentVariables page; the
  C-stack sentence is withdrawn.  The issue gets no comment from this
  stream (approval needed); the text is in the report.
- Found on the way: after the make stage of this change, unit_cxx_partial
  and unit_cxx_literals found CxxPartial and CxxLiterals interpreted (the
  objects of the pool older than libcyrt.so, the condition of section 4
  of tests/README); sprite-make --so CxxPartial CxxLiterals and both pass.
- Verification, one file per process under prlimit --as 6 GB, step mode.
  Both backends: unit_expr.py 28 OK (cxx 2 skipped),
  unit_funcpat_narrowed.py 2 OK (py 10.6 s, cxx 0.7 s),
  unit_py_residuation.py 5 OK, unit_long_lists.py (py 1 expected
  failure, cxx 2 OK), unit_setfunctions_bugs.py 22, unit_prelude.py 17.
  C++ backend: unit_typed_expr.py 55, unit_api.py 9, unit_goals.py 46,
  unit_cxx_partial.py 18, unit_cxx_literals.py 11 OK.  Python backend:
  unit_typed_expr.py 55, unit_api.py 9, unit_goals.py 46,
  unit_py_evaluation.py 6, unit_flat2icurry_bindingopt.py 31,
  unit_py_constraints.py 4 skipped, unit_examples.py test_21 (22.3 s)
  OK.  Example 21 ./run on the Python backend: 24.7 s, output equal to
  expected.out.
- Verification of the review round, one file per process under prlimit
  --as 6 GB, step mode.  Both backends: unit_py_residuation.py 6 OK,
  unit_expr.py 28 OK (cxx 2 skipped), unit_long_lists.py 3 OK (cxx 1
  skipped), unit_curry2icurry.py 15 OK (25 s each).  Python backend:
  unit_py_fairness.py 9 OK (6.2 s), unit_cache.py 28 OK, unit_configure.py
  13 OK.  The probes: tests/unit_py_residuation.py's blockedBehindHead
  spun on the Python backend before the ready() fix (timeout 40 s) and
  suspends at once after it; the old $## step did not end within 20 s on
  pick, pickFirst and picksFirst.  Through the runner (-j 2 --mem 4, both
  backends): the prepare pass on cxx 12 of 12 (530 s); the named and the
  new files 26 of 26; the unit suite 166 of 166 run with two files to
  update: unit_conda.py expected the old configure sentence and the icurry
  route without the front end (both cases now expect the new sentence;
  icurry with the front end and --curry2icurry icurry is the configured
  case), and test_coverage of unit_py_runtime.py expected the TypeError of
  make_node for raw_expr(Int, 1, 2), which the #63 change turned into the
  CurryTypeError of the builder.

2026-10-06: the docs pass for the milestone (worktree wt/docs)
- The build.  make -C docs html at 28f3bf5b finished with 131 WARNING
  lines and 11 ERROR lines; after this pass it finishes with 0 and 0.  The
  causes and the fixes: (1) autodoc_default_options had imported-members,
  so every import of every module was described, the classes of
  curry.icurry.types three times (the package, curry.icurry.types and the
  defining module) and io.StringIO once, with its malformed docstring; the
  option is gone, and a module with __all__ (the curry package itself)
  still documents the names it lists.  (2) The "Attributes:" section of a
  class docstring became attribute directives, which described each slot
  of a class with __slots__ a second time (Scheme, Instance, Pred, Goal,
  Bindings, Defaulting, Spec, Var, the Variable of the Python backend);
  napoleon_use_ivar = True renders the section as a field list.  (3) The
  Reference tree excludes curry.backends.llvm, three empty __init__.py
  files (build-api.bash passes the exclude pattern to apidoc).  (4) The
  two-line terms of a definition list in the sprite-exec page and in the
  docstrings of stats.stats and stats.scheduler_fields are one line each
  (the two ERROR lines of the page and the six of the docstrings).  (5)
  The docstring of interpreter/optimize.py quoted interpreter.import_.load
  without backticks, a reference target in RST.  (6) The Raises sections
  of compile.compile and eval.eval name ~curry.exceptions.CompileError and
  ~curry.exceptions.EvaluationError, because the C++ materializer and the
  C++ bindings define classes of the same names (four ambiguous
  cross-references).  (7) The Introduction had no target for `Fair
  Scheme`_ (targets are per document).  (8) html_static_path named a
  directory that does not exist.  No warning remains.
- The pages.  PythonAPI/UsingTheAPI.rst: every output taken from a session
  on the C++ backend (the reprs of symbols and types are unqualified,
  <curry constructor 'S'>; inspect.types gives a dict of unqualified
  names; a type has no module(); getimpl gives the bytecode of an
  interpreted module); save and load described by backend (the Python
  form on the Python backend, C++ source and a .so on the C++ backend; a
  saved program runs under install/bin/python, it has no interpreter
  line); the goal and the multi-valued examples made consistent (fib3,
  Fib.main, print(value)); the error catalogue with one example per class
  (MismatchError, ConversionError with the None line for Maybe,
  AmbiguousTypeError, DefaultingError, NoInstanceError, FreeFunctionError,
  ArityError, ValueTooLargeError, the five ExprTypeError subclasses,
  InterfaceError), the whole defaulting table, the converters and the flag
  defaultconverter, the markers (one free marker is one variable, two
  choice markers are two choices), the bindings of where-free values as
  Bindings objects, the item error of an iterator, the limits.
  CurryPackage.rst: the top-level names complete (typeof, describe,
  topython, show_value, the markers), the flag table with the defaults of
  flags.py, curry.context removed (no such module), curry.expressions and
  curry.typecheck added.  Architecture.rst: the Context object replaced by
  IBackend as backends/__init__.py defines it.  REPL.rst: the two options
  of :set as the code has them (backend, internal-error-details) and a
  transcript of a real session; rotation and interpret are not :set
  options.  sprite-exec.rst: -i, -t, python -m curry, the first-run time
  (0.64 s for Peano), the --stats line of a real run.  sprite-make.rst:
  the commented LLVM block removed, the saved program run under the
  installed Python; the man text of tools/make.py (a docstring) names
  sprite-make --icy in its examples instead of curry-make --icurry.
  EnvironmentVariables.rst: SPRITE_DISABLE_SYSLIB_CHECKS,
  SPRITE_ENABLE_BREAKPOINT, SPRITE_FORCE_RECOMPILE_CXX,
  SPRITE_WORKTREE_ROOT, the test variables (SPRITE_TEST_MAX_VMEM_KB,
  SPRITE_TEST_FLAGS, SPRITE_UPDATE_EXPECTED) and the build variables
  (SPRITE_REBUILD_ICY, DEBUG, TRACE, COUNTERS, GC, GC_WRITE_COUNTERS,
  JOBS, PREFIX).  ConfiguringSprite.rst: --with-cxx-postinstall in the
  table, the default of --with-python is python3, the other option groups
  named.  Status.rst, new and linked from the index: the pinned front
  end, the two backends and the retirement plan of #82, the interpreter
  modes, the typed boundary, the records by label and file, the known
  limits, what is experimental, the milestone and the epics by number.
  Performance.rst: the harness commands, the two records by label with
  the Phase 2 throughput table from the committed file, the step costs
  from the dated entries, the nightly job, no claim against other
  compilers.  CurryPrimer/Semantics.rst, which was an empty outline:
  choices, call-time choice, overlapping rules, free variables,
  functional patterns, set functions with the escape rule of #61, every
  value from a run.  CurryPrimer/Syntax.rst: the patterns of isNull in
  parentheses, the store as a type synonym, find with its Data constraint
  (the front end rejects the signature without it), the two values of a
  duplicate key.  Introduction: non-strict, not strict.  Quickstart: the
  reprs, the save section by backend, the saved program under the
  installed Python.  Preface note [2]: the front end takes a fraction of a
  second for a small module, not several seconds.  Examples.rst: example
  21 runs on both backends (unit_examples.py), the first run of an example
  measured (0.56 s), the reason the scripts of 03 and 04 pick the Python
  backend.  BuildingDocumentation.rst: the guide is published at
  andyjost.github.io/Sprite (HTTP 200 checked), and the local build needs
  the staged install.  ProjectLayout: scripts/, TODO and NOTES.
  StepByStepBuild: the truncated sentence and PACKS.
- Verified by running: the API session scripts (api/session1.py,
  errors.py, errors2.py, quickstart.py, semantics.py in the scratch
  directory), the REPL with the transcript's lines on stdin and with
  commands on the command line, sprite-exec with -g O, -g '', -m, -i, -t,
  --stats and no module, sprite-make --so, curry.save and curry.load on
  both backends, the first and second run of example 01 from a copy
  without products, and the unit files unit_api.py, unit_compile.py,
  unit_make.py and unit_inspect.py on both backends through the runner
  (-j 2 --mem 4): 8 of 8 run; unit_inspect.py on cxx failed twice at first
  because the pool module Peano had no compiled object in this worktree
  (the condition of tests/README section 4), and passed after sprite-make
  --so tests/data/curry/Peano.curry, 27 tests.
- What the code must change for a page to be true, or what a page now
  says instead: curry.typeof of the goal of compile(mode='expr') without
  exprtype reports a bare type variable ('a'), because the front end built
  the node and no type is recorded for it; the page says so, and a record
  of the defaulted type of a text goal would let it say 'Int'.  The
  interactive prompt of sprite-exec without a module binds no name curry
  (__main__.py passes {'__package__': curry} as the namespace), although
  the help says the prompt starts "in module 'curry'"; the page says to
  import curry there.  A saved program has no interpreter line, so the
  old chmod +x form fails under sh; the pages run it under the installed
  Python.  curry.save on the C++ backend writes C++ source that
  curry.load refuses (it expects .so); the pages describe the forms by
  backend, and the decision on save and load for the C++ backend stays
  with #82.  The REPL has no -h and no :set option for rotation or
  interpret.  Outside docs/ and not changed here: examples/README lists
  21-cxx-types-overloads as cxx alone while unit_examples.py and the README
  of the example run it on both backends; raw_expr(plus, choice, choice)
  evaluated ends the process (a dictionary-less + ; the warning on the
  page covers it); a stray directory curry/.curry/fe-test that a timing
  probe of the front end wrote into the staged install was removed.
- Open.  The Reference pages of the C++ bindings (_cyrtbindings, an
  extension module) are not generated, so the classes it defines appear
  nowhere; the Semantics page is short and has no section on residuation
  beyond the free-variable rule; the docs show the C++ backend's outputs,
  and a run of the same scripts on the Python backend would give the
  differences page by page.
- The review of the pass, applied the same day.  (1) Status.rst no longer
  enumerates the open issues of the milestone: #62, #63 and #85 had closed
  by the time of the review (gh: open 9, closed 3), and the list would rot
  with every closed issue; the page names the milestone and the epics, as
  a list, and points to the milestone page.  (2) The set-function examples
  of the Semantics primer are definitions of the module (main1 to main5),
  because the prompt of the REPL sees the loaded module and the Prelude
  alone: commands.imports() returns the loaded module, so `sortValues
  (set1 f 1)` at the prompt of a module that imports Control.SetFunctions
  fails in the front end with "`sortValues' is undefined", while the
  definitions give [1, 2]; [1, 2] [5, 6]; [1] [2]; (1, [1, 2]) (2, [2,
  3]); True (run at the prompt).  REPL.rst states the scope rule under
  :load with the two ways around it, a wrapper in the loaded module and a
  re-export (module M (module M, module Control.SetFunctions) where),
  both run, and ends with a section "Limits of the REPL".  (3)
  CompileError, EvaluationError and InstantiationError derive from
  BaseException, so `except Exception` does not catch them, while
  CurryTypeError is a TypeError (run: neither CompileError nor
  EvaluationSuspended is caught by `except Exception`); UsingTheAPI.rst
  says so at the CompileError example, at the EvaluationError paragraph
  and at the head of the error catalogue, and CurryPackage.rst at its
  errors paragraph.  Whether the hierarchy should move under Exception is
  a code decision, not made here.  (4) The rule and the default of
  recursion_limit, step_budget, stack_limit and rotation have one prose
  description, the entry SPRITE_INTERPRETER_FLAGS of
  EnvironmentVariables.rst (label sprite-interpreter-flags), beside the
  table of CurryPackage.rst and the docstrings of flags.py; the copies in
  sprite-exec.rst (Selecting a Backend, Bounding One Alternative),
  UsingTheAPI.rst (Limits) and Status.rst (Known limits) are one sentence
  and a cross-reference each.  (5) The long sentences are split: the str
  conversion bullet, the five ExprTypeError subclasses, the suites and the
  step costs of Performance.rst and the epics are lists; a scan of the
  added prose finds no sentence above 40 words outside a table and a
  quoted error message (28 before).  (6) The UnknownTypeConstructorError
  line quotes the session of the page, run in the page's order: "searched
  the interfaces of Prelude, Peano, Data.Maybe, Data.List, Fib".  (7)
  BuildingDocumentation.rst: Sphinx keeps the output of a vanished source
  and make -C docs html has no clean step, so make -C docs clean comes
  before make -C docs html after a change to the set of Reference pages;
  a clean build here removed the three stale curry.backends.llvm pages.
  (8) Status.rst: the ICurry cache is off by default outside the test
  drivers, and configure --cache turns it on.  (9) REPL.rst shows each
  exchange once: the transcript keeps the option listing, the load of a
  module and the backend switch, and the sections keep the rest.
- The build and the tests after the review.  A clean build (make -C docs
  clean, then make -C docs html) finishes with 0 WARNING and 0 ERROR
  lines in 18 s.  Through the runner (-j 2 --mem 4 --backend both):
  --fast ran 122 of 122 files with 4 failures at first (unit_cxx_bom.py,
  unit_cxx_literals.py, unit_cxx_partial.py and unit_cxx_rewrite.py on
  cxx: no cxx.shlib in the metadata of a corpus module, which tiered
  execution interpreted because this fresh worktree had never run the
  prepare pass; the condition of tests/README section 4 for those files,
  not an effect of the docstring edits).  After ./run_tests
  --prepare-only (1236 modules in 12 directories, every product present,
  610 s) --fast ran 122 of 122 with 0 failures (220 s).  --changed selects
  every file, because src/python/tools/make.py maps to no test: 208 of
  208 ran with 2 failures, func_goal_defaulting.py on both backends,
  whose two oracle tests (test_text_header, test_glob_characters) run
  PAKCS, which writes under /tmp and failed where /tmp was read-only
  ("No permission to create directory /tmp/pakcs_file_N"); with /tmp
  writable the file passes on both backends (53 tests each).
2026-10-06: the review pass over examples 13 and 15 and the applications suite
- The committed record of the applications suite is complete,
  tests/data/curry/benchmarks/results/apps-2026-10-06-applications.jsonl,
  31 records.  The partial file of the first attempt (13 records, the
  dependency item at 10 to 50 packages, no overloads item) was replaced:
  the command of results/README.md (-r 3, --timeout 600, --cap 4G, the
  label apps-2026-10-06, resolvelib from a virtual environment on
  PYTHONPATH, under prlimit --as 4 GB) ran through resolvelib 800/solvable
  in 97 minutes, then reached the wall-clock limit of its session during
  resolvelib 800/unsolvable; the overloads item and that item ran after it
  into the same file.  Numbers: cxx 0.083 s against resolvelib 0.38 ms at
  10 packages, 2.0 s against 1.2 ms at 50, timeouts at 100 on both Sprite
  backends; py 5.6 s at 10, 463 s at 50; resolvelib under 10 ms at every
  size and case but 50/unsolvable (30 ms) and 800/unsolvable, where it
  raises ResolutionTooDeep at its bound of a million rounds 430 to 470 s
  into each run, a failed item in the record; overloads cxx 2.08 s per
  1000 calls, py 80 s, plain Python 28 ms, the answers equal on every
  side.  results/README.md describes the
  record; its load note says that the other stream and the test runs of
  this pass kept the load average between 2 and 8 (the py seconds at 10
  and 20 packages came out 4 to 80 percent above the interrupted run at
  load 2).  TestCommittedApplications expects this record: a Sprite
  backend past the timeout at a size is a timeout record, which compare
  --strict reports as a failed item, so the test expects status 1 from the
  strict compare when the file holds one and checks the text ("0 slower",
  "N failed", "0 with changed counters"); it checks that the two sides
  agree on whether a plan exists, that every plan holds on its index
  (depindex.holds), that the plans of cxx and py agree, and that the plans
  of cxx and resolvelib agree at 10 and 20 packages and differ at 50; it
  reads the probe of an item only when its status is ok.
- The overloads item built the eight candidates of the overload set on
  every call inside the measured time, where the Python side read a
  constant and the driver of example 21 builds a set once for several
  calls.  overloads.py builds the overload set and the hierarchy once
  before the clock starts; the terms of the two arguments of a call stay
  inside the time.  The C++ backend went from 6.25 s to 2.08 s per 1000
  calls at the same 8.67 million steps: the difference was the
  construction of 16 terms per call in Python.  tests/README section 9
  and the docstrings of suites.py and overloads.py say so.
- The clang probe wrote "constexpr bool cN = requires { f(aI, aJ); };" at
  namespace scope: a requirement that is not dependent and fails makes the
  program ill-formed, and the workload has ambiguous calls and calls
  without a viable function.  The unit declares "template<class A, class
  B> constexpr bool call = requires (A a, B b) { f(a, b); };" and one
  instantiation call<A, B> per call; the argument variables are gone.
  Checked with g++ 13.3 -std=c++20 -fsyntax-only, the compiler at hand:
  the unit of 1000 calls and the baseline compile, the old form fails
  ("call of overloaded f(float&, float&) is ambiguous"), and 200
  static_asserts of the viability of the instantiations against the
  plain-Python ranking hold.  clang++ is not installed on this machine, so
  the clang path itself is untested; the unit test holds a unique, an
  ambiguous and a non-viable call.
- The resolvelib provider orders the open requirements as the example
  provider of resolvelib does (the package with the fewest candidates
  first) and resolvelib backjumps, where Deps.preferred takes the demands
  in the order it meets them.  Observed at 50 packages, solvable: the two
  sides find different plans (21 packages in the closure of the plan of
  cxx, 25 in resolvelib's; pkg022 2 against pkg022 3); at 10 and 20 the
  same plan.  The docstrings of dependency.py and depindex.py,
  tests/README section 9 and results/README.md say so.  depindex gained
  holds and parse_plan, with tests.  The docstring of depindex described
  the old example 15 (a version for every package of the index); both
  sides assign a version to the closure.
- Example 15: a conflict at a root demand read "the roots needs app 5";
  the verb agrees now ("the roots need app 5, and the index has no such
  version"), and ./run runs that case as the sixth of its seven runs,
  pinned in expected.out.  README 15 names the applications suite as
  present and points to the record; README 21 points to it too.
- Example 13: the README names the 40-character subject of the .*b.*
  measurement (25 x's, b, 9 x's, b, 4 x's; 440 hits, every substring with
  a b) with the times observed on it: 0.4 s on the C++ backend (68 MB)
  and 14 s on the Python backend.
- Rewraps: two paragraphs of README 21, one of README 18, the comment of
  perf.yml, the dependency paragraph of tests/README section 9.
- Verification.  Examples 13 and 15 by ./run on both backends under
  prlimit --as 2 GB and timeout 120: 13 took 0.18 s on cxx and 1.1 s on
  py, equal to expected.out; 15 took 1.7 s and 3.3 s, identical on both
  backends, the new block pinned.  Through the runner (-j 2 --mem 4, both
  backends): unit_examples.py 21 tests OK on each backend (py 132 s under
  load, 85 s later; cxx 22 s and 12 s); the final ./run_tests --changed
  after the record landed, 10 of 10 files on both backends in 99 s:
  unit_benchmarks.py 73 tests on each backend (TestCommittedApplications
  on the complete record among them), unit_examples.py 21, unit_cxx_heap.py
  10, unit_cxx_passthrough.py 18, unit_cxx_variable.py 5.  By hand:
  the applications, options, suites and records classes of
  unit_benchmarks.py (43 tests) on cxx, the two smoke tests on py (32 s),
  unit_perf_history.py 10 OK (4 skipped).  A dry run of ./run_tests
  --changed during the record run found unit_cxx_heap.py and
  unit_cxx_passthrough.py failing on the C++ backend on a stale Tak1.so
  and an interpreted CxxPassThrough (objects compiled under another
  installation; the tiered default never recompiles them in a test
  process); ./run_tests --prepare-only --backend cxx over data/curry and
  data/curry/benchmarks (64 s) fixed both.
2026-10-06: a functional pattern binds a free argument (=:<= against a free variable)
- Symptom.  A function with a functional pattern, called with a free
  variable in that argument position, gave no value on the C++ backend and
  EvaluationSuspended on the Python backend, where PAKCS binds the
  variable: with rule (stem ++ ".o") [stem ++ ".c"] = "cc", the goal
  rule "vec.o" ins where ins free is {ins=["vec.c"]} "cc" in PAKCS 3.4.1.
  The desugaring written out, rule7 (stem ++ ".o") ins | [stem ++ ".c"]
  =:<= ins, failed the same way; the variable on the pattern side (rule8,
  ins =:<= [stem ++ ".c"]) and the strict guard (rule1, ins =:= [...])
  bound it, and the ground call rule "vec.o" ["vec.c"] answered.  The
  owner met it on the pattern rule of example 24, through a set function
  over a free variable created inside the function: an empty set.
- Cause.  The front end matches the outer constructors of the argument
  by cases, which narrow a free argument, and writes the functional
  sub-patterns as one =:<= with the pattern on the left: rule_FREE1 is
  cond ((stem ++ ".o", stem ++ ".c") =:<= (x, y)) "cc" with stem free.
  So the step of =:<= meets a constructor or a primitive value on the
  left and a free variable on the right.  nonstrictEq_step (constr.cpp)
  narrowed the variable over the type of the left side without a guide,
  rts->hnf(C, &rhs, lhs.target->info->type).  The type of Char, Int or
  Float has one constructor, the value node, and instantiate made a
  generator from it: Node::create_flat writes a fresh variable into the
  one slot, which is unboxed, so the pattern 'v' met a Char node with a
  pointer for its value, _equate_rec compared the two and failed, and no
  alternative was left.  nonstrict_eq (constr.py) called
  rhs.hnf(typedef=lhs.typedef) with no values, and hnf suspends a free
  variable of a builtin type without values.  =:= had neither defect:
  constrEq_step passes make_guides (the one value of a primitive, or the
  type of a constructor) and constr_eq passes the value.
- Fix.  nonstrictEq_step passes make_guides for a free right side, as
  constrEq_step does: a free Char, Int or Float takes a value binding
  (ValueBinding, as under =:=), and a free variable of a data type is
  narrowed over the constructors of the type as before.  The codes with
  an unboxed side are covered too: a free side against an unboxed value
  takes the value (make_guides reads an unboxed cursor; its assert on
  'p' is gone), and the one error left, an unboxed value against a node,
  cannot occur, because the two sides of one =:<= have one
  representation (the format of =:<= is "pp", and _equate_rec compares
  the primitive types itself before it recurses); no test reaches it.  A
  free left side (the NonStrictConstraint path) and two free variables
  are unchanged.  nonstrict_eq passes the value of a builtin type to hnf,
  as constr_eq does.  apply_binding, the other maker of =:<= nodes, puts
  a generator on the left, whose head normal form is a constructor, so
  its path is the same as before.
- Tests.  tests/data/curry/FunPatFreeArg.curry: rule, rule7, rule8,
  rule1, cand f t = (ins, f t ins) where ins free, count (xs ++ [0])
  [length xs] over Int lists, wrap n (Just (n + 1)), double (Circle r)
  (Rect (2 * r) (2 * r)) over a small data type, upper (c : _) (toUpper
  c) over Char, each with a form that creates the variable inside;
  FunPatFreeArgSet.curry reads those forms through set functions (set2
  cand f t, set1 countC xs, ...), a module apart so that the PAKCS
  oracle, which has no Control.SetFunctions here, loads the first.
  unit_funpat_free_argument.py asks each goal as a text goal with where
  ... free (the values carry the bindings), as the set-function form,
  and as a ground call with a match and a mismatch; the expected values
  are those of PAKCS 3.4.1 on every goal.  Before the fix: on the C++
  backend 5 failures (every free-argument goal gave []) and 1 error, on
  the Python backend 6 errors (EvaluationSuspended); after: 7 tests pass
  on both.  The Char text goal: Sprite leaves ch in the value, because
  Char occurs in the result type String (goals.absent_from_result), where
  PAKCS prints {ch='V'} "upper"; the test asks (ch, upper "vec" ch) where
  ch free and gets ('V', "upper").
- Regression, through the runner (-j 2 --mem 4 --backend both):
  unit_cxx_freevars.py, unit_cxx_scheduler.py, unit_cxx_mps.py,
  unit_py_residuation.py, unit_funcpat_narrowed.py, unit_cxx_interp.py,
  unit_funpat_free_argument.py and func_funpat.py, 16 of 16 ok;
  unit_examples.py (examples 14 and 24 among them), 2 of 2 ok with 21
  tests per backend; then every unit_cxx_*.py and unit_py_*.py file, 68
  of 68 run with 6 failures on cxx, all the stale-object condition of
  tests/README section 4 after the make stage of this fix (no cxx.shlib
  in the metadata of a corpus module in unit_cxx_bom.py,
  unit_cxx_literals.py, unit_cxx_partial.py, unit_cxx_passthrough.py and
  unit_cxx_rewrite.py; the stamp of Tak1 in unit_cxx_heap.py), and 6 of
  6 ok after sprite-make --so of those corpus modules.  The counts the
  files pin (the forks of unit_funcpat_narrowed.py, the steps of
  unit_cxx_freevars.py between the stress mode and the plain run) did
  not change: those programs call their functional patterns with ground
  arguments, where the path is the one of before.  The documentation
  states no limit on a free argument of a functional pattern (grep
  "functional pattern" in docs/source), so no page changed.
- Review of 2026-10-06, applied.  (1) The corpus had only non-linear
  patterns, so the plain desugaring, one =:<= without a tuple and without
  a shared variable (for single (Just (id 3)) the front end matches Just
  by a case and writes id 3 =:<= x for the sub-pattern), and the Float
  branch of make_guides and of is_builtin_type had no test.
  FunPatFreeArg.curry gained single (Just (id 3)) = "three"
  with singleC, a CAF that creates the variable inside, and half n
  (fromInt n / 2.0) = "half" with halfC; FunPatFreeArgSet.curry gained
  singleS (set0) and halfS; the file gained test_linear and test_float.
  PAKCS 3.4.1: single m where m free is {m=Just 3} "three", singleC is
  (Just 3,"three"), half 3 x where x free is {x=1.5} "half", halfC 3 is
  (1.5,"half"); single (Just 4), single Nothing and half 3 2.0 have no
  value.  Sprite agrees on both backends.  (2) test_char pinned the
  where-free display rule of goals.py, the text "upper" of the plain
  goal, which unit_goals.py pins already; the plain goal now checks one
  value that ends in "upper", and the pair pins the binding.  (3) The
  narrowing of a free actual of a data type over every constructor (the
  Left item below): no change.  The figures of the review, measured again
  on the C++ backend in step mode with curry.stats: rule7 "vec.o" ins
  where ins free 207 steps and 26 forks against 141 and 10 for the ground
  call, rule1 the same 207 and 26, rule 195 and 26, wrap 3 m where m free
  22 and 2 against 16 and 0, single m where m free 11 and 2 against 6 and
  0; the Char and the Float path fork not at all ((ch, upper "vec" ch)
  where ch free 132 steps, half 3 x where x free 37 against 33).  The
  Python backend forks the same.  After the review the file has 9 tests,
  ok on both backends.
- Regression after the review, through the runner (-j 2 --mem 4
  --backend both).  ./run_tests --changed selects every file (the two
  corpus modules are unknown paths to its rules): 210 of 210 run in 617
  s, 5 failed, none from the change.  func_goal_defaulting.py on both
  backends: the PAKCS oracle could not write under /tmp, which was
  read-only in that run; 53 tests ok per backend with /tmp writable.
  unit_inspect.py on the C++ backend: the object of Peano was stale after
  the make stage of the fix (testGetICurry and testGetImpl expect the
  module loaded from its object); 27 tests ok after sprite-make --so
  Peano.curry.  func_flat2icurry.py on both backends,
  test_products_on_disk: the CxxInterp.fcy under
  tests/data/curry/.curry/pakcs-3.4.1 of this tree had the equality of
  hasDup unrewritten (==) where the .icy beside it has constrEq, the
  state the docstring of the test names, and the main tree had the same
  pair (its .icy of 2026-10-05, its .fcy of 2026-10-06); 6 tests ok per
  backend after python -m curry.toolchain.flat2icurry.rewrite
  CxxInterp.fcy ("1 equality replaced").  Then
  unit_funpat_free_argument.py 2 of 2 ok (9 tests per backend),
  unit_examples.py 2 of 2 ok (21 tests per backend), and ./run_tests
  --fast --backend both 124 of 124 ok in 151 s.
- Left.  A free variable of a data type is narrowed over every
  constructor of the type and the wrong ones fail, where the rule of
  Antoy and Hanus binds it to c x1..xn at once; =:= does the same, and a
  direct binding would save the forks (the figures above are the before
  numbers of such a change).  PAKCS prints the binding of rule8
  unevaluated, {ins=["vec" ++ ".c"]}, Sprite prints {ins=["vec.c"]}.  The
  where-free rule leaves a variable in the value when its type occurs in
  the result type, where PAKCS reports it.  A module of the shared pool
  under tests/data/curry is an unknown path to RULES of the runner
  (tests/lib/testrunner/selection.py), so --changed selects every file
  for this change.  What wrote the unrewritten CxxInterp.fcy of the main
  tree on 2026-10-06 is not known.

2026-10-06: example 24 rewritten, the rules in the head (worktree wt/ex24c)
- The shape.  The makefile of examples/24-build-system is a plain Curry
  module without imports or type signatures.  The variables are
  definitions with the uppercase names of make (CC = "cc", DEBUG = "0",
  CFLAGS = if DEBUG == "1" then ... else ...), which the pinned front end
  accepts.  A rule is an equation of the relation rule target inputs =
  recipe, with the inputs in the head: the pattern rule is the non-linear
  functional pattern rule (stem ++ ".o") [stem ++ ".c"] = "...", a
  conditional rule is a guard, rule "libgeom.a" ["plot.o"] | WITH_PLOT /=
  "" = "", and the header edges are the relation depends, one equation
  per edge.  The library asks the relation with the inputs free,
  candidate rule t = (ins, rule t ins) where ins free, through set2, and
  both backends bind the inputs (the fix of #98 on this branch).  The
  interpolator of Make.curry expands $@, $<, $^ and $(NAME) from the
  table the driver passes; $(NAME) is strict, anyOf over the entries of
  the name, so a name with two entries is two recipes and a name without
  one is no recipe.  The lints: ambiguous, missing, and the recipes of
  each target (two values or none, with Make.undefined naming the
  variables without a value).  Make.noDepends serves a makefile without
  the relation; Prelude.failed in its place fails the whole build.  The
  include scanner of the driver is gone.  The driver substitutes every
  NAME=value of its command line into the text of the makefile (the
  first line that starts with NAME and = is replaced, other such lines
  are dropped, a missing one is appended), writes the text under
  .curry/overrides/<sha1 of the text> beside the real makefile, puts
  that directory first on curry.path and imports the module from there;
  a second run with the same assignments finds the products (import
  1.06 s cold, 0.17 s warm in the probe).  The variables are read by
  introspection: dir(module), curry.typeof(symbol) in ('String',
  '[Char]'), every value of curry.eval(symbol, converter='topython');
  dir lists the module's own symbols only, so the variant makefiles are
  full copies with one change each (an import-based variant would hide
  its variables from the introspection).  The table goes to Make.build
  and into the environment of every recipe.  Each recipe is printed when
  it starts, as make prints it; the status table, the stand-in for a
  missing compiler and --serve are gone; -j defaults to 1, as in make.
- The toy.  C only: main.c, vec.c, shape.c, report.c (the report.cpp of
  the design prototype turned into C), the optional plot.c, vec.h and
  shape.h.  main.c includes shape.h and vec.h, plot.c includes shape.h,
  so every edge of depends is a real include (cc -MM agrees).  WITH_PLOT=1
  adds plot.o to the library and -DWITH_PLOT to CPPFLAGS, and the program
  prints plot: (0,0) (4,0) (0,3) as its third line.
- The oracle.  GNU make 4.3, make -rn in a clean environment (env -i) on
  a Makefile of the same rules in the scratch directory:
  identical lines in the default world (6, the double space of the empty
  $(CPPFLAGS) included), with DEBUG=1, with WITH_PLOT=1 (7, ar rcs
  libgeom.a vec.o shape.o plot.o: the library puts the inputs of the rule
  with the recipe before the prerequisite lines for this), and after
  touch shape.h (4).  Make's behaviour on the lint cases, for the README:
  two explicit recipes, "warning: overriding recipe for target" and the
  last wins; an explicit rule beside the pattern rule wins in silence;
  CFLAGS assigned twice, the last in silence; $(LDFLAG) undefined,
  nothing in silence; a missing vec.c without a prerequisite line for
  vec.o, "No rule to make target 'vec.o', needed by 'libgeom.a'.  Stop.";
  with the line vec.o: vec.h make counts vec.o as made and prints the ar
  line; the cycle, "Circular shape.h <- demo dependency dropped." and the
  build goes on.
- The run.  Ten steps, eleven make.py processes and two runs of demo:
  the build from scratch with the program's output, nothing to do, touch
  shape.h with --affected and the rebuild, DEBUG=1 -n in a fresh copy,
  WITH_PLOT=1 built in a fresh copy with the third line, the ambiguity
  lint (Makefile_ambiguous.curry), the recipe lint (Makefile_twice.curry,
  CFLAGS twice), the toolchain (Makefile_toolchain.curry, CC = "gcc" ?
  "clang": the recipe lint on five targets, then CC=clang -n in a fresh
  copy), the cycle (Makefile_cycle.curry -n), the missing source.
  Makefile_choice.curry is gone.  The undefined $(NAME) case has no step;
  checked by hand on both backends in a scratch copy: "no recipe for
  demo: $(LDFLAG) is undefined", exit 2.  Also by hand: a makefile
  without depends plans with Make.noDepends; CC=false gives "failed:
  main.o report.o shape.o vec.o" and exit 2; -j 4 prints the recipes in
  the same order as -j 1 (the trace shows the ready sets).
- Times and tests.  ./run on the C++ backend: 10.3 s cold (the library,
  five makefiles and three substituted copies converted), 4.8 s and 6.9 s
  warm; on the Python backend 19.4 s cold, 15.5 s and 16.8 s warm; the
  output identical on both backends, expected.out regenerated.
  tests/unit_examples.py: C++ backend 21 tests OK in 37 s (24-build-system
  6.1 s), Python backend 21 tests OK in 99 s (16.0 s).  A second run of
  the file after the README and driver edits: 21 OK on both backends,
  28.7 s and 127 s.  The direct runs after the last line wraps matched
  expected.out on both backends, 6.1 s and 18.4 s.  The other lane
  shared the machine.
- Review pass, the same day.  Applied: the output of a recipe is shown
  when it completes, stdout to stdout and stderr to stderr (before, the
  driver captured both and showed stderr on failure only, as the driver
  before the rewrite did, so a warning of cc on a compile that succeeded
  was lost); a program that cannot start is a failed recipe with the
  status 127 and the error of the system on stderr, not a traceback from
  the thread pool; $< expands to nothing on a rule without inputs, as in
  make (before, head [] left the recipe without a value and the lint
  printed "no recipe for X:  are undefined"); the driver prints "no
  recipe for X" when the recipe has no value and no $(NAME) is undefined;
  the README says that /= is not-equal, that a command-line assignment
  drops the other assignments of NAME, and that make's "No rule to make
  target 'vec.o'" holds without a prerequisite line for vec.o, where the
  .d-style line vec.o: vec.h makes make -rn plan the ar line and the
  build fail at ar (GNU make 4.3 again, env -i).  Not applied: the
  toolchain step stays as adapted (CC = "gcc" ? "clang" is the recipe
  lint, then CC=clang on the command line); a set of plans from ? would
  need one variable table per value, and the introspection cannot tell ?
  from a variable defined twice, which the decided lint refuses; the
  owner decides the form, and the 15 lines of that lint stay until then.
  expected.out is unchanged: the toy's recipes print nothing on success
  (cc -Wall on every flag set of the run, checked by hand), and ./run
  matched it on both backends (8.9 s cxx, 24.8 s py, the other lane on
  the machine).  tests/unit_examples.py: 21 OK on cxx (30 s) and on py
  (153 s); run_tests --changed --backend both: 2 of 2 ok.
- Left.  GitHub still lists #98 as open though the fix is on py314.  The
  front end warns on the makefile (overlapping rules of rule and depends,
  no type signatures, a nullary with two equations in Makefile_twice) and
  Sprite's import is quiet, so a reader never sees the warnings; the
  set-function lints say what fires.  A command-line assignment replaces
  one line, so an assignment that spans lines in a makefile would leave
  its continuation behind.  The override cache under
  project/.curry/overrides grows by one directory per distinct
  substituted text and is never pruned.

2026-10-06: G8, the growth factor of the collector (#83)
- The knob.  The collector of the default build (make GC=wdgc;
  graph/memory.cpp includes gc/blockheap.cpp and gc/wdgc.cpp unless
  SPRITE_GC_MPS is defined) sets its node threshold after a collection to
  the growth factor times the survivors, not below the configured
  threshold (2^20 nodes), and its configuration threshold to the factor
  times the live configurations, not below one eighth of the configured
  threshold.  So the heap stays within the factor times the live nodes,
  and the mark cost per allocation is 1 over the factor minus 1.
  GC_DEFAULT_GROWTH is 8; SPRITE_GC_GROWTH overrides it when the runtime
  loads.  gc/mps.cpp (make GC=mps, the experiment behind a gate) replaces
  both files in a build and never links into the default one; its
  g_growth (8, floor 2) reads the same variable and scales the
  configuration threshold after a full collection alone, since MPS
  schedules its own collections.  So the two defaults are one knob for
  the configuration threshold only.
- Measured: the memory suite of the harness (run_benchmarks -s memory
  --variant collector=on -b cxx -r 3 -w 1 --cap 4G --timeout 600 -e
  SPRITE_GC_GROWTH=N -e SPRITE_INTERPRETER_FLAGS=interpret:off, the 28
  dissertation programs that compile on this machine, PokerChoice and
  PokerFree need currypp, plus QueensSet9) at growth 8, 12 and 16: three
  measured repetitions after a warm-up and one under perf for the
  instructions, the modules compiled by sprite-make --so first, one
  harness invocation per factor and one program at a time, on a machine
  shared with two other lanes (load average 1.9 to 4.1).  Collections,
  nodes marked and steps are exact and agree between the repetitions; the
  instructions are the one perf repetition of each item, deterministic
  within the tolerance of the compare tool (1 percent); the seconds are
  indicative.  The factor touches a program only when the survivors of a
  collection times the factor exceed the threshold floor (131,072
  survivors at growth 8, 65,536 at 16), or when the live configurations
  times the factor exceed one eighth of the floor, since the
  configuration threshold follows the same rule: 18 of the 29 programs
  kept their collections and their nodes marked exactly at every factor,
  with the same peak RSS, among them Queens10 (38 collections), Primes
  (47), Tak1 (30), Psort (283) and Quicksort (96); Horseman and QueensSet
  kept their collections, and QueensSet, below the node floor, moved its
  nodes marked by 4 percent and its peak RSS by 8 percent through the
  configuration threshold.  On the nine others the trade is proportional,
  as the policy makes it.  Growth 16 against 8:
  collections QueensSet9 11 to 6, SearchQueens 19 to 10, PermSort 9 to
  5, PermSortPeano 10 to 5, SearchMAC 78 to 60, Hamming 4 to 3, Fib,
  ReverseBuiltin and ReverseHO 2 to 1; nodes marked -50 percent on the
  four search programs, -29 on SearchMAC, -85 on the two Reverse
  programs; the collector's seconds QueensSet9 1.363 to 1.044 (-23
  percent; the destruction of its dead queues is a per-run term, G9),
  SearchQueens 0.890 to 0.494 (-44), PermSort 0.169 to 0.083 (-51),
  PermSortPeano 0.194 to 0.103 (-47), SearchMAC 0.609 to 0.447 (-27),
  which is 1 to 8 percent of the CPU of those programs; the
  instructions -3 percent on QueensSet9 and SearchQueens, -2 on
  PermSort, -1 on PermSortPeano, SearchMAC and Hamming, -6 on Fib, -10 on
  ReverseBuiltin and ReverseHO, 22 programs the same within 1 percent
  (compare --deterministic); the CPU seconds within the noise (QueensSet9
  0.95, SearchQueens 1.00, PermSort 1.03).  Peak RSS +43 to +53 percent
  on the four programs with a live set near 300 MiB (QueensSet9 330 to
  473 MiB, SearchQueens 307 to 465, PermSort 314 to 481, PermSortPeano
  307 to 468), SearchMAC 66 to 88, Hamming 159 to 168; over the suite the
  sum of the peaks 3557 to 4254 MiB (+20 percent) for 52 percent fewer
  nodes marked (40.6 M to 19.3 M) and 33 percent fewer collector seconds
  (3.79 to 2.55 s).  Growth 12 sits halfway on both sides: -30 percent
  nodes marked, -21 percent collector seconds, +8 percent RSS in sum
  (+22 to +29 on the four large programs).
- Repeated at the end for the two factors that decide, 8 and 16 (the
  records -rerun2 and, for QueensSet9 alone, -rerun3 in g8-records/).
  The other lane of the worktree had put its optimizer pass into the
  shared install meanwhile, so the steps of 28 programs changed between
  the first run and the repeat (Queens10 44,630,642 to 34,164,285,
  SearchQueens 39,256,210 to 35,529,964), and the repeat is a second
  observation of the trade on that state, both factors on the same
  steps; a first repeat failed on a module of that lane that the install
  did not link yet, and one repetition of QueensSet9 at growth 16 died
  with SIGBUS when a product was rewritten under it, so QueensSet9 ran
  alone once more.  The exact columns agree with the first run
  (collections PermSort 9 to 5, PermSortPeano 10 to 5, SearchMAC 78 to
  60, QueensSet9 11 to 6, SearchQueens 19 to 9 on its new steps; nodes
  marked -50 to -53 percent on the four search programs; the
  instruction ratios within half a percent of the first run), the
  collector seconds of growth 16 against 8 are 0.68 on QueensSet9 (1.555
  to 1.059 s), 0.51 on SearchQueens, 0.54 on PermSort, 0.62 on
  PermSortPeano and 0.84 on SearchMAC, and the peak RSS is +43 to +52
  percent on the four large programs (QueensSet9 333 to 477 MiB).  The
  CPU seconds do not follow the instructions (PermSort 1.17 and
  PermSortPeano 1.22 on 2 and 1 percent fewer instructions, QueensSet9
  alone 0.85 on 2.7 percent fewer; the load average 3.5 to 7.0): they
  are indicative, the exact columns decide.
- Decision: the default stays at 8.  The work saved and the memory added
  are the same proportion on every program the factor touches, so no
  factor is a free gain; growth 16 buys half the marking work with half
  again the peak RSS of the programs that use the most memory, and it
  doubles the bound the design states (the heap within the growth factor
  times the live nodes); the saving is 1 to 3 percent of the instructions
  of a run, and the 18 programs below the floor gain nothing at any
  factor; the counters of every nightly record stay where they are.  A
  user with memory to spare buys the saving with SPRITE_GC_GROWTH=16.
  Docs: the SPRITE_GC_GROWTH entry of the environment variables page and
  the collector section of the benchmark README state the measured
  trade, and name the configuration threshold as the second path by
  which the factor touches a program (the review of 2026-10-06 found the
  first text stated the node floor alone).  No code and no test changed;
  unit_cxx_gc.py pins the default (test_growth,
  test_threshold_follows_the_survivors): 23 tests OK on the C++ backend.
  The table, the records, the commands and the load are in the
  perf-program note g8-growth.md and g8-records/ of the notebook.
- Left: the per-run term of QueensSet9 (G9); a benchmark with an old,
  retained live set, where the factor would trade differently (design
  open question 2); the Poker programs need currypp on the machine to
  join the suite.

2026-10-06: O2 and O3, the inliner and the known constructor (#71, #72)
- The pass inline_calls (interpreter/optimize.py; the analysis in
  icurry/analysis/inlining.py) runs between saturate_applies and
  inline_aliases under the key <backend>.opt.inline_calls.  Two rules
  rewrite a saturated call in place, from the inside out, and the result
  is rewritten again (to the depth MAX_DEPTH, 16; the dictionary chain
  takes about five): a call of a non-recursive function whose body is an
  expression of at most inline_budget nodes (a new interpreter flag,
  default 4, 0 turns the pass off; the budget counts the nodes a body
  builds, calls, partial applications, constructors, choices and
  literals, a variable counts nothing) is replaced by the body; a call
  g (K a b) of a non-recursive function whose body is one case on that
  parameter, with a branch for K, is replaced by the body of the branch
  (a branch of at most CASE_LIMIT, 32, nodes), with the fields for the
  pattern variables.  An apply whose head the rewrites expose is
  saturated with analysis.saturate.  The sharing rule (plan 4.3): an
  argument, a field, the whole constructor (when the branch uses the
  parameter as a whole), or a local of the body that is used more than
  once is bound first to a fresh variable of the caller's block (IVarDecl
  and IVarAssign, inserted before the statement), so that it is built
  once; one used once is written at its use; one not used is dropped; a
  variable, a literal and a static string are written at every use.  A
  free variable of the body is a fresh IFreeDecl of the caller's block
  (one per call site, as one per call before; unknown inlines to a fresh
  free variable).  A choice in the body is one choice per call site, as
  one call per site.  A body may bind locals (a let that is not
  recursive); a body with a recursive let (INodeAssign) is neither
  inlined nor rewritten (the patch names a path of a built node).  A body
  with a case, other than the one case of the second rule, a branch with
  a nested case or an exempt, a path of two entries, and a variable the
  body does not bind are not shapes (analysis.body_shape); the case at a
  return position is O4's.  An alias function keeps its body, so
  inline_aliases records its target as before; its callers get the alias
  inlined (one node), then the target.  A call of a user alias of apply
  (app f x = f x) stays, as it stays for saturate_applies: the source
  wrote the indirection, and unit_cxx_partial.py needs it to hand a known
  function value to the apply of the runtime (the first form inlined app
  and saturated the chain, and four tests there lost their subject).
- The body that replaces a call is the body as it was before the pass
  rewrote the callee (analysis.inline_shape copies it when first asked,
  which the pass does before it rewrites a function), after O1: the inner
  calls are then rewritten where the arguments are known.  So x /= y on
  Int resolves: _def#/= inlines to not (apply (apply (== d) x) y), ==
  to ==._#lambda (apply d ()), the apply saturates to _inst#Eq#Int (),
  the known-constructor rule gives the dictionary, ==._#lambda selects
  _impl#==, the applies saturate, and _impl#== inlines to eqInt x y;
  mod x n /= 0 resolves likewise through _def#mod, its local pair,
  divMod, _impl#divMod (the pair (divInt, modInt)) and the selector of
  the second component, to not (eqInt (modInt x n) 0).  Non-recursive
  means not on a cycle of saturated calls within the module (Tarjan over
  IFCall edges; a cycle cannot cross modules); a partial application is
  a value and makes no edge, since the dictionary of an instance names
  its methods and a method that falls back to a class default names the
  dictionary, and the chain needs both inlined.
- Each function records the body a call of it is replaced by under
  all.inline_body as ICurry-JSON (the whole IFuncBody), when the function
  is public, non-recursive and within the limits (RECORD_LIMIT 8 for an
  expression body, CASE_LIMIT 32 for a branch), or when it is private and
  a recorded body of the module names it, in a call or in a partial
  application (analysis.recorded_functions): the selector behind a class
  method and the lambda behind . are private, and the chain needs them.
  So a module loaded from its compiled form, which has no bodies, still
  tells its small functions to its importers; both backends carry the
  metadata in their records (the C++ loader hands the keys and the
  metadata on since O1).  The Prelude records 1246 of its 1670 functions
  (664 expression bodies, 582 single-case bodies, 1.04 MB of text in
  Prelude.so and Prelude.py); the record is what the warm expression
  compile pays for (+4.5 ms).  A copied body that names a symbol of a
  module the caller does not import makes the module join the imports.
- Staleness: the format stamps went to 12 (C++) and 6 (Python), so make
  stage and the first import write every cached module again, once; a
  file of format 11 or 5 is correct but slower, and its record tells the
  modules that import it no bodies.  Found on the way: the product
  directory keeps a product whose stamp is current whatever version of a
  pass wrote it, so the background compiles of the tiered mode during the
  work on the pass left objects of intermediate forms in the install and
  in the benchmark directory, which the first two measurements of the
  table measured; the products were deleted and compiled again.
- Found on the way, two: the C++ emitter could not assign a free variable
  to a Variable (_1 = _2 with _2 a Node * from rts->freshvar()), a shape
  the front end never wrote and the inlining of unknown into a let
  writes ($1 <- freeVal with freeVal = x where x free).  passthrough.py
  no longer lets a free variable on the right side force a Variable on
  the left side (a plain left side copies the pointer), and the emitter
  gives a Variable on the left side the node through its target, as for
  a node the step builds (passthrough.free_variables; compiler.free_vars).
  The bytecode and the Python emitters handled the shape.
- Measured with the harness on the C++ backend, 3 repetitions, perf, other
  work on the machine, the deterministic columns deciding; the notebook of
  the performance program keeps the table, the records and the commands
  (o2o3-measurement.md).  Steps: Queens10 44630642 to 17706900 (-60.3
  percent; 0.325x of the Phase 2 record), Primes 49108670 to 14383977
  (-70.7 percent; 0.234x of the record), Tak1 unchanged, Fib unchanged,
  Reverse 8407963 to 8407940 (the nullary functions two, four and nat16
  inlined into one another).  Instructions: Queens10 0.52x, Primes 0.36x,
  Tak1 1.00x, Fib 1.04x, Reverse 1.01x.  Collections: Queens10 38 to 17,
  Primes 47 to 9.  CPU seconds, indicative, at a load average twice the
  baseline's: Queens10 1.47 to 0.84 s, Primes 1.74 to 0.70 s.  Fib's
  instructions rise 3.6 percent on equal steps: fibgen adds through a
  dictionary parameter, and isFib never forces the sum, so the selector
  + d (one node) inlined to +._#lambda (apply d ()) (three nodes) costs
  two nodes per element and saves no step; a refinement that inlines a
  body larger than the call only when the rewrite makes progress would
  spare Fib and keep the chains, at the price of the budget rule as the
  plan states it (four 10 with four x = (x + 1, 2) would stay a call); the
  owner decides.  Gate O after O2 and O3: steps Queens10 at most 0.5x
  and Primes at most 0.4x met, instructions Queens10 and Primes at most
  0.8x met, the latency within 10 ms met, Tak1 and Fib at most 0.8x wait
  for O4 (nothing of O2 or O3 touches their hot loops).  The counters of
  the nightly record change (a compare of the nightly set against the
  Phase 2 record, one repetition, steps and forks): Queens10 54505073 to
  17706900, Primes 61364444 to 14383977, Reverse 8407963 to 8407940,
  SearchQueens 41373250 to 34001063 with the forks 1084883 to 1084670,
  QueensSet 2152979 to 2089740 with the forks 511583 to 511430, QueensSet9
  12143480 to 11784795 with the forks 2979048 to 2978740, PermSort
  12531154 to 12530839 (forks equal), Last 3100017 to 3100015 (forks
  equal); Tak1 and Fib equal.  The forks of a search move because the
  rotations fall elsewhere when the steps change.
- Tests: unit_optimize_inline.py (31 tests on both backends, with the
  helpers of unit_optimize.py): the analysis on hand-built ICurry (the
  node count, the expression and the single-case shapes, the shapes that
  are not, the recursion test with a partial application of the function
  itself and a call into another module, the shape from the metadata and
  from a text that does not read back, the copy of the body, the inliner
  with the sharing rule and the budget, two variables bound to one
  parameter, the known-constructor rule with a shared field, a whole used
  once and twice, a dropped field and a constructor without a branch); the
  shapes of a module (the budget boundary at four and five nodes, a
  recursive function and a mutually recursive pair left alone, a choice, a
  call and a variable as the argument, a choice in the body, a free
  variable, a dropped argument, a local used twice and once, the recorded
  bodies, a private function recorded when a recorded body names it); the
  single-case shapes (a choice in a field, literal fields, a field used
  once, an unused field, the whole parameter used once and twice, the
  lifted inner case of a function of two patterns, a constructor of the
  Prelude); values against the unoptimized program on both backends (the
  same text under another name, imported from its ICurry object with the
  key of the pass set), the flag at 0, the steps saved, the generated code
  of the backend under interpret off; the bodies across modules through
  the metadata of a compiled module and the import join; the chain (x /= y
  to not (eqInt x y), mod to modInt, not True to False, no apply, _def#,
  _inst# or _impl# left; three steps for not (eqInt 1 2)); the installed
  Prelude and its ICurry (no apply with a known head remains after the
  passes, so no nullary _impl# that unfolds to a partial sits in a
  saturable position; every _inst# function recorded; the class default
  inlines the selector); the chains of Queens10 and Primes.
  unit_optimize.py's icurry_of leaves the inliner off unless asked (the
  tests of the earlier passes read the structure it rewrites), its
  compiled() takes flags, and the alias tests that read the code or the
  steps run with inline_budget 0, as does the cross-module test of
  unit_optimize_applies.py.
- Suites: the unit suite on both backends (86 files each) and the oracle
  suites (21 files each) pass.  func_flat2icurry.py found a FlatCurry
  product of the pool written before the binding optimization rewrote
  such files (CxxInterp.fcy, from the setup of the worktree, with no
  constrEq); sprite-make --rewrite-flat --json CxxInterp brought it up to
  date, and the test passes.  Five unit files of the C++ backend changed for
  the reason the passes exist: unit_cxx_gc.py and unit_cxx_heap.py pinned
  the walk over [1..n] at about 24 nodes per cell, and a cell costs 7 now
  that the comparison of enumFromTo resolves (the walks are longer: eight
  million cells in place of three to run out of memory under 1 GiB, two
  million in place of one for more than ten collections);
  unit_cxx_rotation.py pins the counters of psort 8 (17093 to 16929
  steps) and countQueens 8 (2180142 to 2112920 steps, forks 511409 to
  511484: the rotations fall elsewhere); unit_inspect.py reads the
  bytecode of main = double 21 with inline_budget 0; unit_cxx_partial.py
  passes with the alias rule above.  The products of the pool and of the
  benchmark directory were deleted and compiled again where background
  compiles of intermediate forms of the pass had written them.
- Found in the review (2026-10-06), one: the background compile of the
  tiered mode ran under the default budget whatever the interpreter's.
  tiered._environment handed the child backend, interpret and debug
  alone, so a module interpreted with inline_budget 0 was swapped to code
  compiled with the inliner on (a probe: s 1 2 with s x y = y + x cost 3
  steps before tiered.wait and 2 after it), and the flag that serves
  bisection was defeated under the default mode; test_steps of
  unit_optimize.py raced the compile of its module.  The child gets the
  budget of the interpreter (inline_budget:N in its flags), the flag's
  text says so, test_budget_reaches_the_child of unit_cxx_tiered.py pins
  the steps of a call before and after the swap at budget 0 and at the
  default, and test_steps of unit_optimize.py runs under interpret 'new'
  as the steps tests of the other optimizer files do.  Two: a body that
  binds two variables to one parameter ($1 <- $0[0]; $2 <- $0[0], a shape
  _block_parts accepts and the front end does not write today: none of
  the 3190 functions of the installed library and the test pool has it)
  defeated the sharing rule: each variable was bound alone with its own
  use count, so both x = (x, x') on the argument f 1 placed the one call
  object at both uses, and a choice there would have been built twice;
  the whole of a single-case body named through a second variable placed
  the field expressions inside the whole and at their own uses besides.
  The inliner binds the variables of one parameter, or of one field,
  together, with their uses counted together (Inliner.bind takes the
  variables; _by_index groups them), as the fields of a branch were
  counted; test_two_variables_one_parameter of unit_optimize_inline.py
  pins the expression and the single-case form.  No library product
  changes, since no function has the shape.  Three: the results README
  of the benchmarks still said that a compare against the Phase 2 record
  expects equal counters; it names the eight programs whose counters O2
  and O3 moved.  Four: the entry above counted 29 tests in
  unit_optimize_inline.py where the file had 30 (test_alias_of_apply
  joined after the count), and the Python backend had not run the final
  file; both backends ran it (31 tests with the new one).  After the
  review changes, through the runner (-j 2 --mem 4): the --changed
  selection on both backends (42 files, 84 jobs), the eleven unit files
  named above with unit_cxx_interp.py, unit_expr.py, unit_prelude.py,
  unit_benchmarks.py, unit_cxx_heap.py, the collector files and
  unit_cxx_tiered.py on both backends (22 jobs), the oracle suites on
  both backends (21 files each, outside the sandbox of the session) and
  the fast tier on both backends (62 files, 124 jobs) pass whole.  The
  pass changed, so the five programs were compiled and measured again
  (after-review.jsonl in the notebook): steps, forks and collections
  equal to the table above on every program and the instructions within
  0.01 percent (compare --deterministic: 5 same, no counter changed); the
  nightly counters equal to the record of this entry on all ten programs;
  the warm expression compile 21.1 ms (base 18.9 ms), its instructions
  as in the first run.
- Left: a single-use let of a constructor at the argument of a
  single-case function (let p = (a, b) in snd p); the case at a return
  position (O4); a dictionary that arrives as a parameter (Y10); the
  thunk growth of Fib (above); the size of the record (a smaller encoding
  than the ICurry-JSON, or a record limited to what an importer can reach
  with a literal constructor).

2026-10-07: a set function over a variable a functional pattern bound (#97),
  and the measurement of #86 after it
- Symptom (the issue, filed at 15:14 PDT on 2026-10-06 from the exploration
  of example 24).  Module Probe2: g (s ++ ".c") = length s, inner x =
  sortValues (set1 g x), direct (s ++ ".o") = length (inner (s ++ ".c")),
  main4 = [direct "vec.o"]: std::bad_alloc after six seconds at the 2 GB
  cap on the C++ backend, where the Python backend answers [1].  On this
  branch (d54c092b) the module answers on both backends: main3 and main4
  give [1] in 0.3 s (297 and 285 steps, 29 forks), in step mode and in
  time mode, under interpret:new and interpret:all, and with inline_budget
  0.  The fix of #98 (321111e7, merged an hour after the issue was filed)
  changed the path a Char bound outside the capsule takes inside it, and
  hid the symptom for this module.  The two defects behind it stayed, and
  other shapes reach them on this branch.
- Cause one: the capsule does not see the bindings of the enclosing
  configuration.  A binding is private to the configuration that made it
  (Configuration::bindings).  The capsule of a set function is a nested
  configuration with an empty map, and get_binding read that map alone,
  where the decisions of the enclosing configurations are read through the
  queue stack (read_fp, walk_qstack), and where the comments of evalS_step
  and holds_private_state describe the values of the nested evaluation as
  depending on the bindings of the configuration that runs it.  So a
  variable of the
  argument that the enclosing configuration bound looked unbound inside
  the capsule.  Against a literal, a Char or an Int was bound anew, to the
  literal, and the comparison held whatever the outer value: with endsInC
  (t ++ "c") = length t and outerChar (s ++ ".o") = sortValues (set1
  endsInC s), outerChar "cx.o" gave [1] on both backends (PAKCS 3.4.1 on
  the plain goal: no value).  Against a constructor, a list was narrowed:
  instantiate put a generator on the shared variable node, the choice
  escaped (its id joined the escape set through the guard), and the
  enclosing configuration, which never decided it, forked on it.
- Cause two: fork applies a binding without end.  A choice with the id of a
  variable is the generator of that variable, and fork applies the binding
  of the variable to each alternative, (generator =:<= binding) &> e
  (apply_binding), so that the side agrees with the binding.  The
  constraint pull-tabs the same generator node to the root, the
  configuration forks on it again, update_fp finds its side already in the
  fingerprint and says yes, apply_binding wraps the root again, and so on:
  one configuration clone, two pull-tabs and the wrap per round, no step
  ever completes the alternative.  The docstring of the Python step says
  "Pop a binding ... That binding will be removed from the table"; neither
  implementation removed it.  The Python step named the symbol
  rts.prelude.nonstrictEq, which the Prelude handle does not expose (the
  function is private), so it raised AttributeError whenever it ran: the
  Python backend never applied a binding at a fork.  Before #98 the
  unguided narrowing of a bound Char inside the capsule made such a
  generator (the garbage Char of the #98 entry), which is the route the
  module of the issue took to the loop (read from the code of e52ece9d,
  not run).
- Measured on this branch before the fix, sprite-exec --stats, step mode,
  prlimit 2 GB, timeout 120.  endsIn0 (ys ++ [0]) = length ys and
  outerList (id xs) = sortValues (set1 endsIn0 xs): outerList [1, 2, 0]
  std::bad_alloc after 4.8 s at 1.86 GB, 20734141 steps, 6911375 forks, 3
  collections; outerTail (id (x : xs)) = sortValues (set1 endsIn0 xs) on
  the same list: 7.9 s, 1.95 GB, 30086332 steps, 10028771 forks; outerInt
  (id (x : xs)) = sortValues (set1 endsIn5 (x : xs)) with endsIn5 (ys ++
  [5]) = length ys, on [3, 5] and on [5, 3]: the same counts as outerTail.
  Without a set function, sibling = (x =:<= [1, 2] &> (length x ? 7)) ?
  len0 x where x free (len0 by two rules): the C++ backend prints 7, 1, 0
  and dies on the alternative that reads x (20734140 steps, 6911380
  forks); the Python backend raises on every one of these goals ("module
  'Prelude' has no attribute 'nonstrictEq'").  outerChar "cx.o": [1] on
  both backends.
- The fix, on both backends.  (1) get_binding reads through the queue
  stack as read_fp does: the map of the configuration, then the map of the
  front configuration of each enclosing queue, under the group id of the
  variable in that configuration (RuntimeState::get_binding in
  rts_freevars.cpp; get_binding and has_binding of rts_bindings.py through
  _find_binding).  is_void and holds_private_state read through it, so a
  nested configuration that waits on a variable the outside bound is
  ready, and a capsule whose goal holds such a variable is private.  The
  writers (add_binding, apply_binding, update_binding) use the map of the
  configuration alone.  (2) apply_binding consumes the binding it applies
  (write(C->bindings).erase; del config.bindings.write[gid]): the
  constraint carries it into the variables of the generator, and a
  configuration that forks on the generator again applies nothing.  The
  Python step names the symbol through rts.symbol('Prelude.nonstrictEq').
  After the fix every goal above gives the value of the oracle on both
  backends in about 0.2 s: outerList [2] (84 steps, 7 forks), outerTail
  [1], outerInt [1] and [], sibling 0, 1, 2, 7, outerChar "cx.o" [] and
  "xc.o" [1]; the module of the issue [3], [4], [1], [1] (main3 270 steps
  and 24 forks on the C++ backend, 297 and 29 before: the capsule reads
  the bindings of the stem instead of binding its characters anew).
- Tests: tests/unit_setfunctions_bindings.py over
  tests/data/curry/SetFunctionsBindings.curry, five tests on both
  backends, the children under prlimit 1 GB and timeout 120 (a regression
  spins inside C++): the four goals of the issue, the bound Char with its
  plain goals, the bound Int field and tail, the bound list in both forms,
  and the sibling alternatives.  The expected values are those of PAKCS
  3.4.1 on the plain goals (plainChar "cx.o" no value, "xc.o" 1; the list
  goals 2 and 1; sibling 2, 7, 0, 1 through len0, since a case on the free
  variable suspends in PAKCS, where a rule narrows).  Two tests more after
  the review: test_private_capsule (the bound character read by a case
  and by a comparison inside a private capsule, with the plain goals), and
  test_shared_capsule, a known failure (unittest.expectedFailure) on both
  backends, which asserts the values of the plain goals for the three
  shared shapes of the item left below.
- #86 after #97.  The shape of unit_funcpat_narrowed.py measured again with
  curry.stats on both backends (one, many and distinct through source,
  sourceS and sourceN): every count equal before and after the fix.  C++:
  many 219524 steps and 1202 forks through source, 12102 and 8 through
  sourceS, 177745 and 1202 through sourceN; distinct 9529/324, 1690/72,
  10231/450; one 145, 138, 162 steps, 8 forks each.  Python: many 12867,
  12882, 13468 steps with 1202, 8, 1202 forks; distinct 1333/324, 1504/72,
  1594/450.  The cause of #97 was not a resolution the capsule performs
  once; it was the read of the enclosing bindings and the loop at the
  fork, both outside the restart per read of #86 (one configuration, its
  own bindings, copy_spine from the root at every read).  So the check the
  plan asked for does not apply, and nothing changed for #86: the idiom
  and the pinned counters stay; the options of the entry of 2026-10-06
  stand.
- Left, the owner's item.  A capsule shared by enclosing configurations
  that bind a variable of its argument differently after the capsule
  started: the first configuration that runs the capsule puts its binding
  into the private spine of the nested configuration (copy_spine and
  E_RESTART, as for its own binding), the value goes into the shared
  allValues node, and the other configuration reads a value made with the
  first's binding.  Measured with the shapes of section 5 of the test
  module, on both backends, on this build and on the build of d54c092b:
  gCase c = 0 ? hCase c with hCase a case on c, gEq with a comparison
  c == 'a', the capsule started by select while c is free, then (c =:<=
  'a' ? c =:<= 'b') &> selectValue rest.  Before this change both shared
  goals suspended ("Evaluation Suspended!"); after it both alternatives
  give (0, 1), where the plain goals give (0, 1) and (0, 2) (PAKCS 3.4.1
  agrees).  So for these shapes the change replaces a suspension by a
  wrong value in the sharing configuration.  The functional-pattern shape
  (gPat s = 0 ? endsInA s, s bound to "xa" or "xb" after the start) died
  before the change, in the loop of cause two on the C++ backend and in
  the AttributeError on the Python backend, and gives (0, 1) twice after
  it, where the plain goal gives (0, 1) alone.  The private shapes (c
  bound before the capsule starts) suspended before the change and give
  the values of the plain goals after it.  The escape of #61 does not
  cover the shared capsule: a binding has no choice node to escape.  Two
  routes.  (a) The
  pre-#98 route made sound: the capsule does not read an enclosing
  binding of a data type; it narrows the variable, the generator choice
  escapes, and the enclosing fork applies the binding once (the fixed
  apply_binding); sound for a shared capsule, one split and one restart
  per bound variable the capsule meets, and no route for a Char, an Int
  or a Float, which have no generator (a ValueBinding is a constraint, not
  a choice; a choice over it needs the machinery of get_generator and
  constrain_equal).  (b) An ownership test for the capsule (private or
  shared), which the runtime lacks (the entry of #61): a private capsule
  reads the bindings as now, a shared one takes route (a).  The walk of
  this entry is the rule of the dissertation's comments made true for the
  private capsule, which holds_private_state makes of every capsule whose
  argument holds a bound variable at creation; a clone made after the
  creation shares the spine, so it holds the same capsule.
- Verification.  The new file alone on both backends (5 tests, 2.8 s on
  cxx, 1.5 s on py).  Through the runner (-j 2 --mem 4 --backend both,
  with --prepare after the header change: 201 modules in 5 directories,
  every product present): unit_setfunctions_bindings.py,
  unit_setfunctions_bugs.py, unit_cxx_scheduler.py, func_setfunctions.py,
  unit_funcpat_narrowed.py, unit_funpat_free_argument.py,
  unit_cxx_freevars.py, unit_py_residuation.py, unit_cxx_interp.py,
  unit_cxx_mps.py, func_funpat.py, unit_examples.py, unit_cxx_gc.py,
  unit_cxx_counters.py, unit_cxx_variable.py, unit_cxx_passthrough.py,
  unit_cxx_stack.py, unit_cxx_heap.py, unit_py_fairness.py,
  unit_py_copy.py, unit_cxx_gc_counters.py, func_kiel.py,
  unit_cxx_partial.py, unit_cxx_literals.py, unit_cxx_bom.py,
  unit_cxx_rewrite.py and unit_inspect.py: 54 of 54 run, 0 failed, wall
  286 s.  The pinned counters of unit_funcpat_narrowed.py and the steps of
  unit_cxx_freevars.py did not move.  In the stress mode of the collector
  on the C++ backend: unit_setfunctions_bindings.py 5 tests and
  unit_setfunctions_bugs.py 22 tests (1 skipped) pass.  Not done:
  tests/manifest.json has no entry for the new file (a calibration run on
  a quiet machine adds it); the issue gets no comment from this stream.

2026-10-07: the bug batch, #99 and #96, and the lint lines of example 24 (worktree wt/bugs)
- #99, a FlatCurry file written again beside an up-to-date .icy.  The
  writer, found with a probe of the front end (A imports B imports C, B
  with a required equality): a run of the front end on a module compiles
  an import again when the interface (.icurry) of the import is older
  than the interface of one of its own imports, or than its source, or
  when its files are missing; a touch of C.icurry alone does it, a touch
  of C.fint does not.  The run writes the .fcy, .fint and .icurry of the
  import in the text of the front end; the ICurry of the import is
  current by the rule of the toolchain (its source did not change), so
  nothing translates it again; and the route rewrote the file of the
  named module alone.  The main tree shows the mechanism: the install's
  Prelude.icurry was written by a make stage at 10:20 on 2026-10-06,
  CxxInterp.fint and .icurry at 15:26 (a run of the front end on a module
  that imports CxxInterp; unit_cxx_interp.py compiles one from text), the
  .icy of 2026-10-05 untouched, and the hand rewrite of 16:28 changed the
  .fcy alone.  So a make stage that writes the interfaces of the library
  anew makes every module of a tree such an import at its next contact.
  A second writer, outside the toolchain: the PAKCS oracle.  :load M runs
  the front end, which compiles the stale modules of the chain, and :eval
  runs it once more, for the goal, with --acy --flat (parseProgram and
  writeAndParseExpression of c2p.pl), which compiles every module without
  an .acy file again, after BindingOpt rewrote the modules it compiled to
  Prolog: in the probe B.fcy was the text of the front end, without
  constrEq, after :load A :eval main.  No .acy file is under tests/data
  in this worktree or in the main tree, so the oracle wrote no pool
  module there of late.
  The fix.  _frontend.curry2flat takes the stamps (size, time) of the
  .fcy files under .curry/<subdir>/ of every search directory but the
  system library before the run (42 files in the pool, 22 in the library)
  and rewrites every file that is new or changed after it: the module's
  and those of the imports the run compiled again.  A file of an import
  that cannot be read is reported through the log and left; the module's
  own file raises as before.  rewrite=False gives the text of the front
  end to the tests that need it (fresh_flatcurry of
  unit_flat2icurry_bindingopt.py, two tests of unit_curry2icurry.py).
  Both routes go through it; the icurry branch of the converter lost its
  own call of optimize_flatcurry.  The on-disk check:
  flat2icurry_oracle.check_file takes accept_unrewritten; a pair that
  differs is tried once more with the pass over the program in memory,
  and when the pass changes the program and the optimized program
  translates to the .icy, the result is EQUAL with the mark unrewritten
  and a detail.  test_products_on_disk of func_flat2icurry.py passes it,
  names the PAKCS oracle in its docstring as the writer, and prints the
  files that needed it (none in this worktree); the archive is checked
  without it.  Tests: test_import_compiled_again_is_rewritten of
  unit_curry2icurry.py (the A/B/C scenario through the route: the file of
  B keeps constrEq and pairs with its .icy; with rewrite=False the pair
  differs; flatcurry_dirs leaves the system library out), and
  TestUnrewrittenFile of func_flat2icurry.py (the pair of a foreign
  writer, made with the front end alone: DIFFERENT without the keyword,
  EQUAL and marked with it, the file untouched, a real difference stays
  one).  The two usability points: sprite-make --rewrite-flat implies
  --icy and stands alone (the sentence of AGENTS.md holds now; the man
  text and the help say so); run_tests --changed maps
  tests/data/curry/M.curry to the test files whose text names M as a
  word (selection.files_naming; NAMED in RULES), and to everything when
  none does, with the note "no test file names the module M; selects
  everything" (unit_runner.py tests both); on this tree the other lane's
  SetFunctionsBindings.curry maps to unit_setfunctions_bindings.py alone.
- #99, options not taken, for the owner.  (B) Translate an import the
  run compiled again at once and write its .icy when the text differs:
  closes the pre-pass pair too, but a step then writes the product of
  another module, and under the icurry route the import would get a
  product of the port.  (C) An .icy is stale when the .fcy beside it is
  newer: the extraction of the archive (tar -m) gives a pair an arbitrary
  order, and every archived module would be made again at first import.
  (D) An .icy is stale when the .fcy beside it has constrEq and the .icy
  has not: cheap and sound, remakes a pre-pass .icy after a hand rewrite
  without --rewrite-flat; no pair seen needs it.  What changes for a
  pre-pass .icy (the archive's, made by icurry from the text of the front
  end): when the front end compiles such a module again as an import, its
  .fcy gets the rewrite and the pair differs in test_products_on_disk
  until sprite-make --rewrite-flat, where before the change the pair
  agreed on the pre-pass program.  No module of the pool whose file the
  pass changes is imported by another today (the pass changes 7 of the
  1260 files under tests/data; CxxInterp's .icy is post-pass).  Also
  left: src/python/tools/make.py is an unknown path to RULES and selects
  everything.
- #96, the warning on overlapping rules.  The front end prints its
  warnings on stderr in blocks (file:line:col-line:col Warning:, the
  text indented, a blank line between blocks); -W none -W overlapping
  keeps the overlap warning alone (--no-overlap-warn did not silence it
  in the probe, --no-warn does); _system.pexec dropped the stderr of a
  run that succeeded, so no warning reached anyone whatever the flags.
  PAKCS shows the warning by default (parseProgram of c2p.pl;
  warnoverlapping=yes in pakcsrc.default).  The fix: pexec(with_stderr)
  returns the pair; command() adds WARNING_FLAGS (--no-verb -W none -W
  overlapping) unless the caller asked for quiet or
  SPRITE_FRONTEND_WARNINGS says off (0, off, no, false; an empty value
  counts as unset), else QUIET_FLAGS as before; curry2flat groups the
  blocks by source file (warnings_by_file) and logs one record per module
  at the WARNING level through the logger of _frontend ("the Curry front
  end warns on module M (path):" and the text of the front end), which
  the basicConfig of curry.config shows on stderr by default with its
  time stamp.  A module compiled from its products or from the ICurry
  cache runs no front end and warns no more; a run that compiles an
  import again warns on it again.  The other warnings (non-exhaustive
  patterns, missing signatures, unused bindings) stay quiet.  The suites:
  cytest, the runner (cli.environment) and the benchmark harness
  (Settings.environment) set SPRITE_FRONTEND_WARNINGS=0 unless the
  environment sets it; the prepare pass runs sprite-make -q anyway; the
  CI script needs no change, cytest covers every file.  Example 24:
  make.py sets the variable to 0 before it imports curry, because the
  rules of a makefile overlap by design, so ./run shows no warning and
  expected.out does not change for #96; the other examples show the
  warning on stderr on a cold run, which unit_examples.py does not
  compare.  Tests: unit_frontend_warnings.py (the parser and the
  grouping; curry.import_ warns once with the module name and not on the
  non-exhaustive function, then from the products and from the cache not
  at all; curry.compile once and a quiet process not at all; sprite-make
  once on stderr, a second run, -q and the variable not at all; the
  default of cytest in a child process); test_command of
  unit_curry2icurry.py pins the flags.  A first version of the test
  evaluated build 3 with build 0 = []; build n = n : build (n - 1), the
  trap of #36: the second alternative of build 0 is infinite, the
  conversion to Python never ended, and the file ran for nine minutes
  under the 6 GB cap before it was stopped; the program has a guard now.
- Example 24: the recipe lint prints one line per target (show_recipes
  of make.py): the recipes after the count when the line fits in 79
  columns, else the count alone, so steps 7 and 8 print four and five
  lines where they printed twelve and fifteen; the ambiguity lint of step
  6 (one target with its two rules) is unchanged.  expected.out was
  regenerated from ./run on both backends (6.4 s on cxx, 17.1 s on py,
  identical, one line of stderr each from time); the README quotes the
  new lines.  The toolchain step (CC = "gcc" ? "clang" refused by the
  recipe lint, then CC=clang on the command line) stays as the owner
  decided.
- The review findings, applied on 2026-10-07.  (1) The shared capsule of
  the #97 entry: measured on both installs and corrected there; the test
  file gets test_private_capsule and the known failure test_shared_capsule
  (see that entry).  (2) The warning on overlapping rules in the examples.
  A cold sprite-make --icy over a copy of every example directory, with
  SPRITE_FRONTEND_WARNINGS=1 and without the ICurry cache, warns in seven
  directories besides the makefiles of example 24: 04-static-compile (aux
  of wildcard_matching), 05-blocks-in-flask (move of solver),
  10-queens-set-functions (ndinsert of Queens), 12-cryptarithm (ndinsert
  of Crypt), 14-parser-functional-patterns (expr of Parser),
  19-blocks-world-app (move of Blocks) and cxx (insert of CxxLayout,
  constReferee of CxxOverload, which examples 23 and 21 import).  The
  decision, the one example 24 took: the rules overlap by design, the
  choice is the program, so the run scripts of those eight examples
  export SPRITE_FRONTEND_WARNINGS="${SPRITE_FRONTEND_WARNINGS:-0}" (an
  empty value counts as unset) with a comment that names the function,
  and the README of the examples, AGENTS.md and the page of the
  environment variables say so; the variable set to 1 in the environment
  shows the warnings again.  Checked with a cold run of the seven scripts
  the test runs, in a copy of examples/ without products: status 0, an
  empty standard error, the standard output equal to expected.out; the
  same cold run of 10-queens with the variable 1 prints the one record.
  The default of the variable and expected.out do not change.  (3) The
  NAMED rule of --changed selected the files whose text names the module
  alone, so a change to a module of the pool no longer selected
  test_products_on_disk of func_flat2icurry.py, which pairs the products
  of every module of the pool from a directory listing, nor a file that
  reaches the module through an importing module.  Now the rule follows
  the import lines of the pool (selection.pool_importers: import M,
  import qualified M, import M (f), import M as N, through other modules
  of the pool), selects the files that name the module or an importer,
  and adds the files of POOL_CHECKS (func_flat2icurry.py) with their own
  reason; a module that no file names, importers included, still selects
  everything.  select_changed takes pooldir; unit_runner.py tests the
  chain Deep imports PeanoUser imports Peano on a pool of its own, the
  module without an importer, the importer nobody names, and the real
  pair FunPatFreeArgSet imports FunPatFreeArg; section 10 of tests/README
  says it.  On this tree SetFunctionsBindings.curry maps to
  func_flat2icurry.py and unit_setfunctions_bindings.py.  (4) The inline
  branch of show_recipes (the recipes after the count when the line fits
  in 79 columns) appeared in no output and no test: the recipes of the
  example are compiler command lines.  unit_examples.py gets
  TestBuildSystemDriver.test_show_recipes, which compiles the function
  from the source of make.py alone (function_of; the driver imports curry
  and the library Make when it is imported) and checks both branches, the
  inclusive width, and the two recipes of main.o of step 7 (94 columns).
  The steps of the example stay as they are.  (5) The comment on the last
  assertion of test_unrewritten_file_beside_rewritten_icurry described a
  file the pass leaves as it is, where the assertion checks a corrupted
  oracle (the pass changes Guard.fcy; the .icy with constrEq renamed does
  not match).  The comment says so now, and TestUnrewrittenFile gets
  test_pass_that_changes_nothing: a module without a required equality,
  whose pass gives the program back (assertIs), beside an .icy with the
  module name changed, is DIFFERENT with unrewritten False, with and
  without accept_unrewritten; the branch "optimized is not prog" of
  check_file is covered on both sides.
- Verification, one file per process under prlimit --as 6 GB, the
  commands of the task.  C++ backend: unit_curry2icurry.py 16 OK,
  unit_flat2icurry_bindingopt.py 31 OK, unit_frontend_warnings.py 5 OK,
  unit_runner.py 65 OK, unit_make.py 18 OK (1 skipped), unit_cache.py 28
  OK, func_flat2icurry.py 7 OK (41 s; no pair needed the acceptance).
  Python backend: func_flat2icurry.py 7 OK (29 s), unit_frontend_warnings.py
  5 OK, unit_curry2icurry.py 16 OK, unit_flat2icurry_bindingopt.py 31 OK.
  unit_examples.py: 21 OK on cxx (2 skipped, 35 s) and 21 OK on py (3
  skipped, 137 s).  ./run_tests --changed --list maps the changed paths
  as section 10 of tests/README says; tests/lib/cytest/__init__.py selects
  everything.  The whole suite through the runner (-j 2 --mem 4 --backend
  both --prepare, without unit_currylib.py, which removes products of
  the shared install) was started and reached job 42 of 238 with every
  file ok but func_goal_defaulting.py on cxx, whose PAKCS oracle could
  not write under /tmp (read-only in the sandbox of the session; the
  entry of #98 saw the same); the run was cut short by the end of the
  session and must run again, func_goal_defaulting.py outside the
  sandbox.
- Left.  The full regression run.  After the findings were applied, the
  edited files passed one at a time under prlimit --as 6 GB:
  unit_setfunctions_bindings.py 7 tests OK with 1 expected failure on the
  C++ backend (2.6 s) and on the Python backend (1.7 s); unit_runner.py
  TestSelection 11 OK; unit_examples.py TestBuildSystemDriver 1 OK;
  func_flat2icurry.py TestUnrewrittenFile 2 OK (C++ backend).  The whole
  suite through the runner (-j 2 --mem 4 --backend both --prepare, every
  unit and func file but unit_currylib.py) was started: the prepare pass
  compiled 1239 modules in 12 directories on both backends with every
  product present, and the first file (unit_cxx_toolchain.py on the C++
  backend, 33 tests, 36.5 s) was ok when the session ended at job 25 of
  238.  The run must be made again, with the func files that use the
  PAKCS oracle outside the sandbox, the fast tier, and unit_currylib.py
  alone.  tests/manifest.json has no entry for unit_frontend_warnings.py
  and unit_setfunctions_bindings.py (a calibration run adds them).  The
  issues get no comment from this stream.

2026-10-07: the dependency cleanup (worktree wt/conda, the dependency lane of #1)
- The inventory.  Every tool and library that configure, the Makefiles,
  the Python package and the C++ runtime need, from configure's checks,
  Make.include, the include lines of src/cyrt, the imports of src/python,
  the apt list of scripts/setup-dev-machine.sh, conda/dev-environment.yml,
  the recipes, and the Installing and Developer Setup pages.  B is build
  time, R is run time; req is required, opt is optional.

    dependency                     when  need  what needs it                              verdict
    Python 3.14 + pyconfig.h       B R   req   configure; the pybind11 extension; the      keep
                                               package (json, mmap, zlib of the stdlib)
    g++                            B R   req B the runtime and the bindings; the C++       keep
                                         opt R backend compiles generated code (without
                                               one, every module runs interpreted)
    gcc                            B     req   configure's CC; .c sources under GC=mps     keep
    GNU make, ar (binutils),       B     req   the build; flock guards the archive;        keep
    flock, realpath, nproc                     realpath in Make.rules and sprite-invoke
    pakcs-frontend 2.0.0           B R   req   Curry to FlatCurry: the library interfaces  keep (pinned)
    (PAKCS 3.4.1)                              at make stage, every compile at run time
    pybind11 (extern/pybind11)     B     req   the bindings of the runtime                 keep
    Boost headers (integer,        B R   req   fingerprint.hpp (int_t, static_log2,        drop: replaced
    pool, utility, preprocessor)               pool), the noncopyable base of the state    (below)
                                               headers, two .def files; the generated
                                               code includes the headers at run time
    PAKCS (pakcs on SWI-Prolog)    B     opt   the oracle of the func tests; --with-pakcs  keep, optional
                                               '' builds without it
    SWI-Prolog                     -     -     runs pakcs and builds the saved states of   not Sprite's:
                                               the PAKCS binary distribution              the docs say so
    Haskell Stack                  -     -     builds PAKCS from source                    not Sprite's:
                                                                                           the docs say so
    icurry 3.1.0 (cypm)            B R   opt   the alternative route to ICurry             keep; looked for
                                               (--with-icurry, SPRITE_CURRY2ICURRY)        on request only
    sqlite3 program                -     -     cypm keeps its index in SQLite; nothing     drop the check
                                               of Sprite runs it (the cache uses the
                                               sqlite3 module of the stdlib)
    ccache                         B R   opt   --with-ccache: the build and tools/cxx      keep, optional
    jq                             -     -     nothing: gone since #16 (verified)          gone
    curl or wget, tar              -     opt   the PAKCS install steps of configure and    keep, optional
                                               the setup script download PAKCS
    git                            B     opt   make's default goal (submodules, overlay-   keep
                                               prune); the setup script
    tree, rsync                    -     opt   make MANIFEST; make -C docs install         keep, unchecked
    stdbuf, socat, rlwrap          -     opt   the editor targets of tests/Makefile        keep, unchecked
    numpy                          R     opt   certain tests (configure: recommended)      keep, optional
    sphinx, sphinx_rtd_theme       B     opt   make -C docs html (configure -D)            keep, optional
    sqlite3 module (stdlib)        R     opt   the ICurry cache (curry.cache, guarded)     keep, optional
    GNU time (/usr/bin/time)       R     opt   the peak memory of a benchmark run          keep, optional
    perf (linux-tools)             -     opt   profiling; the apt list of the setup script keep, optional
    libgmp                         R     req   pakcs-frontend's own library, not Sprite's  not Sprite's

- Boost dropped from the C++ runtime.  Five uses, each replaced by the
  standard library or a few lines: boost::static_log2 by a constexpr
  function (fingerprint.hpp); boost::int_t<N>::exact by the trait
  fingerprints::exact_int<N> over <cstdint> (the same type, int32_t, so
  the layout of Block and the static_assert of Node are unchanged);
  boost::pool<> by fingerprints::BranchPool, a free list over blocks of
  chunks of one size that doubles the block at each refill, as
  boost::pool did (std::pmr::unsynchronized_pool_resource would serve,
  but it finds the pool by size and the chunk by bitmap on every call,
  and the branches are allocated at every copy-on-write of a shared
  fingerprint, so the local pool keeps the O(1) push and pop; it uses the
  throwing ::operator new, where boost::pool returned null); the
  boost::noncopyable base of Configuration, Queue, InterpreterState, Set
  and RuntimeState by deleted copy members (RuntimeState already had
  them; an empty base occupies no storage under the Itanium ABI, so no
  layout changes); and BOOST_PP_CAT, BOOST_PP_STRINGIZE and
  BOOST_PP_TUPLE_ELEM in currylib/defs/not_used.def and unboxed.def by
  local macros with the one indirection each needs (APPLY expands the
  tuple before the element macro sees it; the preprocessed names, the
  strings and sizeof(Node2) are the ones Boost.PP produced).  Gone with
  it: the Boost check of configure (it looked under /usr/include and
  /usr/local/include and wrote nothing into Make.config), the flag
  -I$(BOOST_HOME)/include of Make.include (BOOST_HOME was never defined,
  so the flag was -I/include), the commented Boost blocks of
  Make.validate, libboost-dev in the apt lists of the setup script, its
  test and the CI workflows, and the Boost sentences of the docs and
  AGENTS.md.  The runtime headers changed, so the ABI stamp of every
  compiled module changed; make stage compiled the library again.
- The sqlite3 program.  configure checked for it as a prerequisite of
  icurry: cypm, the Curry Package Manager, keeps its package index in
  SQLite and needs the executable (its manual says so).  Nothing of
  Sprite runs it; the ICurry cache uses the sqlite3 module of the
  standard library (curry.cache, guarded by a try).  The check is gone;
  the installation steps of icurry note the need (the old step named the
  apt package sqlite, which does not exist; it is sqlite3).
- icurry is looked for only on request.  Before, a configure that found
  no front end looked for icurry as well and reported it missing, with
  the sqlite3 program behind it, although the icurry route needs the
  front end too (set_curry2icurry_tool aborts without it).  Now
  --with-icurry or ICURRY asks for it; otherwise it is not configured and
  not reported.  The prerequisite check in a clean environment on this
  machine (PATH=/usr/local/bin:/usr/bin:/bin, the project Python) went
  from "Missing: pakcs, pakcs-frontend, sqlite3, icurry.  Found 4
  problems." to "Missing: pakcs, pakcs-frontend.  Found 2 problems."; with
  the tools of Make.config it says "Found 0 problems." before and after.
- Kept, and said plainly in the docs (ConfiguringSprite, Software
  Compatibility, Developer Setup, AGENTS.md): SWI-Prolog runs pakcs, the
  test oracle, and builds the saved states of the binary distribution;
  Haskell Stack builds PAKCS from source; Sprite needs neither, it runs
  the front end binary.  The help of --with-prolog says the option serves
  the PAKCS build alone.  GNU time stays optional.  jq was already gone.
- For the package agent (conda/ is theirs): recipe/meta.yaml can drop
  libboost-headers from host and run, with the comment that names the
  Boost headers; build.sh can drop the export BOOST (configure reads no
  such variable now) and the -isystem "$PREFIX/include" of the build
  wrapper and of tools/cxx, which existed for the Boost headers (the
  generated code includes the runtime headers of opt/sprite/include
  alone); tests/unit_conda.py then drops its two libboost-headers
  assertions and the '-isystem' assertion; conda/README.md its Boost
  rows and sentences.  Open question 5 (one source of truth for the pin):
  the recipes can read configure at render time with the jinja function
  conda-build already provides, as the version now reads the VERSION
  file: {% set frontend_version = load_file_regex(load_file=
  "../../configure", regex_pattern="PINNED_FRONTEND_VERSION = '([^']+)'",
  from_recipe_dir=True).group(1) %} in recipe/meta.yaml, then
  "curry-frontend {{ frontend_version }}.*" in host and run; and the same
  two lines for PINNED_PAKCS_VERSION and PINNED_FRONTEND_VERSION in
  curry-frontend/meta.yaml, whose version and pakcs_version are the same
  two pins.  Copies that stay: PAKCS_VERSION and ICURRY_VERSION of
  scripts/setup-dev-machine.sh (its PAKCS step runs before a checkout
  exists), the PAKCS_VERSION of ci.yml and the CI script, and the
  literals of tests/unit_conda.py and tests/unit_setup_script.py.
- Not changed: the ABI stamp (toolchain.object_digest) still digests the
  real path of the installation prefix, so shipped objects count as stale
  under another prefix (open question 2 of conda/README.md); a change
  there is the owner's.
- Tests, on this machine in the clean environment (env -i with PATH=
  /usr/local/bin:/usr/bin:/bin, HOME, LC_ALL=C.UTF-8, TMPDIR).  A first
  pass under the login shell, whose conda toolchain exports CXXFLAGS, gave
  two false failures in the precompiled-header names of
  unit_cxx_toolchain.py (O3O2 for O3) and was discarded.  configure
  --check-prereqs before and after: above.  ./configure with the options
  of Make.config: 6.9 s, the icurry check included.  make stage from the
  emptied install and object trees, through ccache with -j4: 1 min 35 s,
  exit 0; the one compiler warning (module.cpp:147, an unused variable)
  predates this change; the library was compiled again under the new ABI
  digest (aa0df224b6c19ac9; the stamps of the shared test pool held
  9df6714a6d1d4f67 from the build of the morning).  Through the runner
  (-j 2 --mem 4 --backend both): unit_configure.py 17 OK on both backends
  (three new tests: the missing front end reported alone, icurry
  requested and missing with the cypm note, and no Boost in src/cyrt,
  configure, Make.include and the CFLAGS of make), unit_setup_script.py
  16 OK on both, unit_fingerprint.py 3 OK on both, unit_cxx_toolchain.py
  33 OK on cxx; unit_conda.py 1 failure on both: test_sprite_recipe
  expects the rm -rf of the precompiled header that the package agent's
  build.sh no longer has (their change in progress; their file).  The
  fast tier, 130 file-backend pairs: 125 OK; the five files that failed on
  cxx (unit_cxx_literals.py, unit_cxx_partial.py, unit_cxx_rewrite.py and
  unit_cxx_bom.py, 'cxx.shlib' missing from the metadata of an interpreted
  module; unit_inspect.py, bytecode where compiled code was expected)
  read the stale objects of the shared pool under the tiered default, the
  case --prepare exists for after a header change; with --prepare (43
  modules of data/curry, 28 s on cxx) all five pass on both backends.  A
  standalone exercise of BranchPool (1000 distinct pointer-aligned chunks,
  the freed chunk handed out first, a fingerprint grown to depth 3 with
  copy-on-write and a move) passed.  make -C docs html: 32 s, 0 warnings,
  0 errors; the regenerated configure-usage.rst carries the new
  --with-prolog text.
- Other files changed, outside the files of this lane: .github/workflows/
  ci.yml and perf.yml (libboost-dev out of six apt lines; CI then checks
  that no Boost include comes back), and tests/unit_setup_script.py (its
  apt list).  Done the same day: the whole suite ran under the new headers
  with --prepare on both backends in the review pass of the two lanes (216
  file-backend pairs; see the end of the next entry), libboost-headers is
  out of conda/, and tests/unit_conda.py checks the recipe without it.
2026-10-07: the conda package installs and runs clean (worktree wt/conda, the package lane of #1)
- The build.  Both recipes built with conda-build 26.9.1 through the new
  script conda/build-packages.sh, from an export of HEAD f9ce8077 with the
  uncommitted files of this lane as overlays (a development build), the
  PAKCS archive from the source cache, CPU_COUNT=4, prlimit --as 16 GB,
  timeout 3600.  curry-frontend-2.0.0-pakcs341_0.conda: 1,499,173 bytes,
  2 min 19 s.  sprite-0.9-py314hc388cfb_0.conda: 2,747,197 bytes, 713
  files, 27 MB installed, 7 min 23 s from the environments to the end of
  its tests (the test step 1 min 1 s); the whole script 10 min 19 s.
  info/has_prefix holds 12 binary entries, the shared objects of the 12
  library modules with imports (the NEEDED paths); no text file names the
  build prefix.  The build of record is the second build of the day, from
  the working tree of both lanes (every changed path of git status as an
  overlay): it carries the dependency cleanup, so libboost-headers is out
  of the recipe and no header of the package includes Boost;
  sprite-0.9-py314hc388cfb_0.conda 2,747,127 bytes, 713 files, 12 binary
  has_prefix entries, 4 min 57 s with the test step 48 s; its environment
  (python 3.14.8, gxx 15.3.0, libstdcxx 16.2.0, sysroot_linux-64 2.39, no
  Boost package) gave the same environment test within 0.3 s: import
  0.11 s, the first import of the Prelude 1.86 s interpreted and 0.13 s
  from the object later, sprite-exec 2.43 s then 2.02 s on cxx, 0.91 s
  then 0.20 s on py, sprite-make --so 30.9 s, both examples as expected,
  the same three objects written, the bytecode caches valid.
- The environment test (conda create from the local channel: sprite,
  python=3.14; the environment resolved python 3.14.8, gxx 15.3.0 through
  cxx-compiler 2.0.0, libstdcxx 16.2.0, sysroot_linux-64 2.39, gmp 6.3.0,
  curry-frontend 2.0.0 pakcs341_0).  Each command in a scrubbed
  environment with the bin of the environment on PATH, no SPRITE_HOME, no
  CXX, XDG_CACHE_HOME in a scratch directory: import curry 0.10 s; the
  first import of the Prelude on the C++ backend 1.66 s, interpreted, the
  shipped object stale (below), a later import 0.18 s from the object the
  background compile wrote; sprite-make --icy of a module that imports
  every library module 0.73 s; sprite-exec of a two-value program that
  imports Data.List 2.31 s then 1.68 s on cxx, 0.75 s then 0.20 s on py;
  python -m curry 2.33 s (cxx), 0.18 s (py); interpret:all 2.29 s;
  sprite-make --so 31.7 s (the stale Prelude, Data.List and Data.Maybe
  compiled first, the precompiled header built in the cache directory,
  80 MB) and the program then 0.2 s from its object; examples 10 and 24
  through their run scripts from copies outside the repository with
  SPRITE_HOME=<env>/opt/sprite (the scripts default to the install of a
  checkout), 0.77 s and 11.9 s, both outputs equal to expected.out; go.py
  and make.py through the Python of the environment with no SPRITE_HOME.
  The dynamic loader: the Python of the environment and the compiled
  program both name /lib64/ld-linux-x86-64.so.2, the sysconfig value.
- The writes.  Under opt/sprite after the runs: the objects and stamps of
  Prelude, Data.List and Data.Maybe (the stale-object recompile, open
  question 2) and the object of Control.SetFunctions without its stamp
  (the background compile of example 10, cancelled at exit).  Nothing
  else: the precompiled header went to $XDG_CACHE_HOME/sprite/pch/<key>,
  the 13 bytecode caches of the library and the 181 of the package were
  valid (bytecode_is_current on every file; conda keeps the modification
  times), and Python wrote its own lib/python3.14/__pycache__/
  _sysconfigdata pyc.  Probe in a second fresh environment with the 13
  ABI stamps rewritten to the digest of that environment, which is what
  the proposal of open question 2 gives: the first import of the Prelude
  0.20 s from the shipped object, sprite-exec 0.69 s then 0.18 s,
  sprite-make --so 3.4 s, nothing written under opt/sprite.
- The ABI stamp (open question 2), not changed, proposed.  object_digest
  digests the real path of the installation prefix (the repair of C6 of
  the audit of #82), so the shipped objects are stale under every
  environment prefix.  The condition of the lane for a change was "small,
  every existing test green": unit_cxx_toolchain.py TestFlavor.test_digest
  asserts that the digest changes with the prefix and equals a 16-digit
  hex string, so no relocatable form keeps that test as it is.  Proposal
  in conda/README.md: the stamp carries the real path of the installation
  as text beside the digest of the headers and the flags; conda-build then
  lists the stamps in info/has_prefix as text, conda rewrites them with the
  NEEDED entries, is_stale compares the text with the real path, a copy by
  hand stays stale, a relocated package keeps its objects (the measured
  gain: 1.66 s to 0.20 s on the first import, 31.7 s to 3.4 s on the first
  sprite-make --so, no write).
- Decided within the lane (conda/README.md records each).  Open question
  3: config.cxx_pch_root puts the precompiled header into
  $XDG_CACHE_HOME/sprite/pch/<key> (else ~/.cache/sprite/pch/<key>; the key
  a digest of the real path of the installation, one directory per
  installation because remove_stale_members would otherwise cross
  installations) when the installed include directory cannot be written or
  lies in a conda environment (a conda-meta directory above it);
  SPRITE_CXX_PCH_ROOT stays the override.  The generated files of the
  library name their sources relative to the installation:
  _filenames.installed_relpath, the Python emitter writes
  filename=curry.config.installed_path('curry/Prelude.curry'), the C++
  emitter writes "curry/Prelude.curry" into the module record and the
  loader resolves it; a source outside the installation keeps its absolute
  path; the format stamps did not change (the old files load as before).
  Question 10: meta.yaml reads VERSION at render time (load_file_regex).
  Question 11: the source of the recipe is the export of a commit through
  build-packages.sh (git archive of HEAD and of the pybind11 submodule;
  --overlay for uncommitted work; --dry-run, --export-only, --skip-frontend,
  --pakcs-archive into the source cache, --memory-limit, --timeout; the
  plan names the passed proxy variables without their values).  Questions
  13 and 14: documented facts with a test of the dynamic loader path
  (unit_conda.py test_ld_interpreter_path reads the PT_INTERP of the
  Python of the installation).  Question 6: conda-forge offers gxx 16.2.0
  but cxx-compiler 2.0.0 pins gxx 15.*, so no environment holds both; a
  probe with SPRITE_CXX from a gxx 16 environment compiled the program in
  4.5 s and ran it with the libstdcxx 16.2.0 of the environment; the member
  name of the precompiled header digests the wrapper, not the compiler
  behind it, so two SPRITE_CXX compilers share a member and g++ ignores the
  wrong one.  The launchers and tools/cxx resolve their own location with
  readlink -f, so a link to a launcher works.  The dependency cleanup of
  the other lane: libboost-headers is out of host and run, the build
  wrapper with -isystem is gone, --with-cxx-postinstall is the compiler of
  the build, tools/cxx adds no flag.
- Files of the lane: conda/README.md, conda/recipe/meta.yaml,
  conda/recipe/build.sh, conda/build-packages.sh (new), tests/unit_conda.py
  (19 tests: the build script's dry run and export, the launcher, the
  header rule, the dynamic loader path, the relocatable generated files),
  src/python/config.py, src/python/backends/cxx/compiler.py, loader.py,
  src/python/backends/py/compiler.py.  Outside the files of the lane:
  src/python/toolchain/_filenames.py (installed_relpath) and
  docs/source/CommandLineInterface/EnvironmentVariables.rst (the rule of
  SPRITE_CXX_PCH_ROOT).  Verified on a private export of HEAD with the
  lane's files (the shared tree's runtime was mid-edit by the other lane),
  one file per process: unit_conda.py 19 OK on both backends (two tests
  skip outside a git checkout), unit_cxx_toolchain.py 33 OK,
  unit_cxx_tiered.py 20 OK, unit_cxx_bom.py 10 OK after sprite-make --so
  of its pool module (a cold tree interprets it under tiered),
  unit_findcurry.py 9 OK both, unit_sigtable.py 22 OK, unit_loadsave.py 4
  OK, unit_goals.py 48 OK both, unit_make.py 18 (1 error: the pool product
  hello.icy of a cold tree), unit_prebuild.py 17 OK both, unit_currylib.py
  10 OK both, unit_examples.py 22 OK both, unit_inspect.py 27 (the two
  known failures on cxx of the 2026-10-05 entry).  Left for the owner: the
  stamp proposal and the split of the package (question 2); the channel
  and the list "What remains before publication" of conda/README.md (the
  front end feedstock, the compiler pin, macOS and Windows, the oracle,
  pip, the notices of the front end, the sysroot pin, a build without
  overlays from a commit that holds this work).  The shared staged install
  of the worktree lost the generated products of the 13 library modules
  (removed for a regeneration whose make stage failed in src/cyrt while
  the other lane edited it); the next make stage writes them again.
- The review findings, applied on 2026-10-07 (the seven findings of the
  review of both lanes; each confirmed by a reading of the code and by a
  run).  (1) A read-only installation.  The shipped library objects are
  stale under every environment prefix (open question 2), so sprite-make
  --so of any program compiles the Prelude again, and when opt/sprite
  cannot be written it failed with a bare "[Errno 13] Permission denied"
  on Prelude.so.abi (the review's probe; the environment of the third
  build with its directories made read-only); and in a writable
  environment the recompile changes six conda-tracked files (Prelude,
  Data.List, Data.Maybe, .so and .so.abi; a hand comparison with the
  sha256_in_prefix of conda-meta), so the entry above understated the
  fact as a start-up cost.  Done: Cpp2So.__call__ checks that the
  directory of its object can be written before the old stamp goes, and
  raises a CompileError that names the directory, the state of the object
  there (none, no stamp, stale) and the way out (make the directory
  writable, or sprite-exec, whose tiered default interprets a module
  without a current object); the stamp of a stale object stays as it is;
  otherwise nothing changes (the compile failed at the first write
  before).  Test: unit_conda.py test_compile_into_read_only_directory
  (cxx, the three states).  conda/README.md states both facts
  ("Relocation", decision 6, open question 2, item 11 of the list before
  publication).  A fallback to the interpreter inside sprite-make would be
  a design change: left to the owner with the stamp proposal.
  (2) The precompiled header of the test step of conda-build went under
  ~/.cache/sprite/pch/<key> of the builder, 151 MB in two keys after the
  two builds of the morning.  build-packages.sh passes
  XDG_CACHE_HOME=BUILD_ROOT/cache in its scrubbed environment and prints
  it in the plan (test_dry_run asserts it); the third build wrote its
  member (74 MB) under BUILD_ROOT/cache and nothing new under
  ~/.cache/sprite.  The two members of the morning are still there (outside
  the directories of the lane; the owner's to remove).  The growth of the
  cache (one directory per installation, none collected) is stated in
  conda/README.md ("The compiler", open question 3) and in
  EnvironmentVariables.rst.
  (3) The comment of meta.yaml above detect_binary_files_with_prefix said
  that the generated Python and C++ name the Curry source.  Rewritten: the
  shared objects alone name the prefix, the text files name their sources
  relative to the installation (step 9), and a test of the recipe checks
  that.
  (4) conda/README.md said that the bytecode caches record the build
  prefix.  The shipped .pyc files record prefix-relative paths
  (co_filename opt/sprite/python/curry/config.py; none of the 194 caches
  of the environment holds an absolute path).  It said that the first
  import writes the recompiled objects.  A short process cancels the
  background compile at exit and writes nothing; sprite-make --so, and a
  process that outlives the compile, write them.  Both corrected (the
  table of the environment test, "Relocation", decision 6, open question
  3).
  (5) build-packages.sh accepted --overlay conda/.. (the plan: rm -rf of
  the whole export, cp -a of the whole working tree) and .. (rm -rf of the
  src directory of the build root).  The path is normalized with realpath
  -m --relative-to, and ., .. and ../* are refused; an absolute path is
  refused as before; conda/./recipe becomes conda/recipe.  Test:
  test_overlay_paths (six refused paths, an absolute path, a missing file,
  a path with . and .. components).
  (6) The artifact records the build root and the channel of this machine
  (info/recipe/meta.yaml: the path of the export and a comment;
  info/about.json: the URL of the local channel; info/has_prefix: the
  placeholder under BUILD_ROOT/bld, as every conda package) although the
  recipes name no path.  Item 10 of the list before publication: a neutral
  path or a CI feedstock, and an inspection of info/recipe and
  info/about.json (conda build --no-include-recipe leaves the recipe out).
  (7) unit_conda.py pinned VERSION to 0.9.  It checks the shape of the
  value now, and that the regex of the recipe reads the same value.
- The third build of the day, the build of record, and its environment.
  build-packages.sh --skip-frontend --memory-limit 16 --jobs 4 with every
  changed path of git status as an overlay, outside the sandbox (/tmp):
  sprite-0.9-py314hc388cfb_0.conda 2,791,180 bytes, 713 files, 12 binary
  has_prefix entries and no text entry, conda-build 3 min 44 s with the
  test step (CPU time 1 min 41 s for the build, 27 s for the tests), the
  script 3 min 59 s.  Against the second build 222 files differ in
  content (the 13 objects and their stamps, which digest the build prefix;
  the 194 bytecode caches, which record the modification times of their
  sources; toolchain.py and its cache, the one change of size, 1,929
  bytes), and the archive is 44,053 bytes larger.  conda create of
  sprite-final from the local channel (5 s; every package cached):
  python 3.14.8, gxx 15.3.0, libstdcxx 16.2.0, sysroot_linux-64 2.39,
  gmp 6.3.0, curry-frontend 2.0.0 pakcs341_0.  The environment test, each
  command under env -i with the bin of the environment on PATH: import
  0.10 s; the first import of the Prelude 1.69 s, interpreted;
  sprite-exec 2.32 s on cxx and 0.64 s on py, and the three short runs
  wrote nothing under the environment; with the directories of opt/sprite
  read-only, sprite-make --so exit 1 in 0.7 s with the new message,
  sprite-exec 1.8 s twice, no file of the package changed; writable again,
  sprite-make --so 30.9 s and the six files, then sprite-exec 0.17 s and
  the Prelude 0.14 s from the new object; the 13 caches of the library and
  the 181 of the package valid, none of the 194 with an absolute path; the
  header 71 MB under $XDG_CACHE_HOME/sprite/pch/<key>; nothing new under
  ~/.cache/sprite; opt/sprite 26 MB on disk.  The shared staged install
  of the worktree holds the products of the library again (the make stage
  of the dependency lane; the stamps carry the current digest
  aa0df224b6c19ac9).
- Verification of the review pass: make stage 6.7 s (nothing to rebuild;
  the Python sources are linked); unit_conda.py 21 tests OK on cxx and on
  py (one skip on py: the C++ toolchain test), one process at a time;
  make -C docs html 16.5 s, 0 warnings, 0 errors.  Through the runner at
  -j 2 --mem 4 on both backends: ./run_tests --prepare --changed selected
  every file (configure, Make.include and config.py changed: "an unknown
  path changed" maps to everything), 24 prepare directories and 216
  file-backend pairs, 2,608 tests, 16 min 48 s; 2 pairs failed inside the
  sandbox, func_goal_defaulting.py on cxx and on py, because the oracle
  (pakcs on swipl) creates /tmp/pakcs_file_<pid> and the sandbox mounts
  /tmp read-only; the same file outside the sandbox: 2 of 2 OK in 10.7 s.
  The fast tier: 130 of 130 OK, 159 s (the stale-pool failures of the
  morning do not return: the pool was prepared under the current digest).
  unit_conda.py through the runner: 21 OK on cxx and 21 OK on py.

2026-10-07: the ABI stamp names the installation as text, and the product cache (worktree wt/cache, issue #100)
- The stamp.  object_digest no longer digests the real path of the
  installation prefix.  The digest covers what decides whether an object
  fits a runtime: the runtime headers (runtime_digest), the flags of the
  flavor and of the collector with its write counters, the link flags,
  the compiler of the build and the format of the generated code (the
  last two since the review of the item, below).  The stamp file
  <module>.so.abi has two lines now: the digest, 16 hex digits, and the
  real path of the installation (installation_path).  Cpp2So.is_stale
  accepts an object when the digest is one of accepted_digests and the
  second line, as a real path, is the installation of the process
  (stamp_is_current); a stamp of one line, the old form, is stale.  So a
  copy of an installation made by hand stays stale (its stamps name the
  original, whose shared objects its objects link against), a package
  whose manager rewrote the text keeps its objects (conda-build lists a
  text file that holds the prefix in info/has_prefix by itself; the next
  build of the conda package must show 12 binary and 13 text entries,
  and the build prefix must be a real path for conda-build to find it in
  the stamps), and two installations of one runtime give one digest,
  which the product cache keys on.  The format is documented on the
  class Cpp2So.  Consequence: every compiled product is compiled once
  more after this lands (the prepare pass and make stage do it); the 43
  modules of the pool of this worktree took 26.5 s with ccache disabled.
  Tests (unit_cxx_toolchain.py, 36 OK on cxx): TestAbiStamp.
  test_stamp_lines (the reader; a stamp of one line, blank space, a third
  line), test_relocated_object_is_kept (the text rewritten, also through
  a link: no compile), test_hand_copy_is_stale (the right digest with
  another path: compiled again, with a stamp of this installation);
  TestFlavor.test_digest asserts now that the digest does not change
  with the prefix and that the text decides.  conda/README.md closes
  open question 2 for the stamp (the split of the package stays open),
  meta.yaml says that the stamps are text entries, build.sh turns the
  product cache off for the build.  The docs: CompilationPipeline,
  WithoutCompiler, sprite-make, the environment variables.
- The cache.  curry.toolchain._productcache; the root is
  SPRITE_PRODUCT_CACHE (config.product_cache_dir): by default
  $XDG_CACHE_HOME/sprite/products or ~/.cache/sprite/products; the empty
  string turns it off, and off is the behaviour before the cache.  An
  entry is <root>/<digest>/<key>/ with <Module>.cpp, <Module>.so and
  <Module>.so.abi.  The digest is the first line of the stamp.  The key
  (_productcache.product_key, KEY_FORMAT 2 since the review below) digests, in order: the
  format of the key; the name of the module (the generated code and the
  symbols of the object carry it; the math corpus holds pairs of modules
  with one text, intmath065 and intmath073 among them, which shared an
  entry and missed in turn until the name joined the key); the facts of
  the backend (Cpp2So.product_facts):
  the format version of the generated code (compiler.FORMAT_VERSION), a
  digest of the sources of the code generator (the C++ emitter and its
  helpers, the generic emitter, the optimizer and the ICurry analyses:
  GENERATOR_SOURCES), the keys of the optimizer passes, the inline budget
  of the interpreter, the intermediate directory, the digest of the route
  from Curry to ICurry (cache.frontend_digest), the real path of the
  installation (the second line of the stamp) and the real path of the
  directory of the source; and the chain digest: the module's own texts
  (the Curry source, the ICurry file and the JSON, each one that exists;
  since the review below) and the name and the text of every module it imports,
  transitively, found in the Curry path or in the library of the
  installation (the Prelude is implicit).  These are the facts that
  decide staleness today (the stamp, the format stamp of the .cpp, the
  texts of the chain) plus the facts that shape the generated code.  The
  two paths are in the key because an object names them: its record
  names the source (relative to the installation for a library module,
  absolute otherwise), and its NEEDED entries name the objects of its
  imports by absolute path (readelf -d of Numeric.so of the install:
  .../install/curry/.curry/sprite-pakcs-3.4.1/Prelude.so and two more).
  An object of another tree restored here would make the dynamic linker
  map that tree's Prelude beside this one (finding C6 of the audit of
  #82).  So the cache serves one tree: the same tree after its products
  were removed, after an edit and its revert, after a branch switch and
  back.  A second worktree gets no entry of the first, and the
  acceptance of #100 (the prepare pass of a second worktree in under a
  minute) is out of reach under the risk line of the item.  What would
  let two trees share, the owner's decision (the link and the record are
  the runtime's design): the link line names the imports by SONAME (the
  full module name) instead of by path, with the imports loaded before
  the module as the loader does today, and the record of a source
  outside the installation names it relative to the object; a new
  FORMAT_VERSION; then 'installation' and 'source' leave product_facts
  (one line each) and KEY_FORMAT goes to 2.  Where the cache is used:
  Maker.make asks the plan before each step (Plan.restore, the step
  before the last, Cpp2So.restore) to place the cached products of the
  module, also before a step that would end the plan at its input, so
  the first import of a module under tiered places its object and loads
  the module compiled instead of interpreting it; Cpp2So.__call__ stores
  after a compile (Cpp2So.store).  sprite-make, the background compile of
  tiered (sprite-make --so in a child) and the prepare pass all go
  through Maker.  A module in the temporary directory of the system, or
  made by str2module, is never cached (excluded); SPRITE_FORCE_RECOMPILE_CXX
  skips the restore; a restore refuses a directory that cannot be
  written and a module whose import runs without an object.
  Concurrency: a store writes into a temporary directory beside the
  entry and renames it into place; a writer that finds the entry removes
  its own files; a restore places each file through a temporary name and
  a rename.  The shared object is hard-linked both ways when the cache
  and the product directory lie on one file system (Cpp2So.__call__
  unlinks the old object before the compiler runs, so a link into the
  cache is never written into; the generated file and the stamp are
  copied, because the emitter writes in place; a refused link falls back
  to a copy).  The restored files get modification times later than
  every file of the module, in the order of the chain, so the object is
  the newest file.  sprite-make prints "sprite-make: product cache: N
  restored, M stored" at the end of a run that did either, not under -q;
  its --jobs children print the line for the parent (--child), which
  sums them.  The prepare pass runs sprite-make without -q and reads the
  line from the log: the line of a directory says "43 of 43 from the
  product cache" and the summary line counts them.  The runner and
  cytest set SPRITE_PRODUCT_CACHE to tests/.cache/products unless the
  environment names it (the empty string included), beside the ICurry
  cache, so a test run writes nothing under the cache directory of the
  user and the restored objects are hard links.  scripts/new-worktree.sh
  no longer copies the C++ products of the main tree (stale in the
  worktree by the stamp and by the NEEDED entries, before this change as
  well); with SPRITE_WORKTREE_PREPARE=1 it ends with the prepare pass.
- Measurements, this worktree, CCACHE_DISABLE=1, env -i, one process at a
  time (ccache would not have helped: its statistics of this host show 5
  percent of the compiler calls cacheable, the precompiled header keeps
  the rest out).  The prepare pass of the pool (43 modules, data/curry)
  with every object stale (the stamps of the old form) and the cache
  empty: 26.5 s, 43 stored, 4.6 MB in the cache.  The products of the pool
  moved aside and the pass again: 0.5 s, 43 of 43 from the product cache
  (0.70 s for the whole run_tests).  The cold pass over the corpora, in
  chunks: math (218 modules) 48.5 s, eqconstr (767) 278.6 s, the other
  nine directories (254 modules with the warm pool) 105.6 s; about 7 min
  40 s for the 1,239 modules of the pool and the corpora, 1,236 entries,
  74 MB (the compiled products of tests/data are 1,242 objects, 42 MB).
  Every product of tests/data moved aside and the pass over everything:
  23.0 s, 1,208 of 1,239 from the product cache; the 31 others were
  compiled, and the reason was the defect above: pairs of modules with
  one text in math and io (intmath065 and intmath073, compare56 and
  compare57, getChar_catch2 and getChar_error2, ...) shared a key, so the
  entry held the files of one name and the other missed, compiled, and
  replaced the entry.  With the name in the key: the pool, math and io
  (284 modules) cold 85.5 s (284 stored), moved aside and the pass again
  3.0 s, 284 of 284 from the product cache (math alone 1.0 s for 218).
  The first import of a pool module under tiered with its products aside:
  Chars 0.061 s from the cache, loaded from the object (restored 1);
  0.037 s with the object present; 0.067 s interpreted with the cache off
  (no object written by the short process); CxxBom 0.059 s, 0.036 s,
  0.057 s; the Prelude from the installed object 0.032 s (import curry
  itself 0.07 s).  So for a small module the restore costs about 25 ms
  over a present object and saves the compile (0.2 to 0.6 s here, more
  for a large module) and the interpreted steps.
- Tests.  tests/unit_product_cache.py, 24 tests, on both backends (12 of
  the compile step skip on py): the root and the empty string, the
  default under XDG_CACHE_HOME and under the home directory, the
  excluded directories; the chain digest and the closure (an import
  changed, the module changed, a copy of the directory gives the same
  chain, an import the path does not hold, a module without a source
  from its ICurry or JSON); the key over the facts; the store, the
  lookup and the restore with stand-in files (the hard link, the copy
  across file systems, a refused link, the replace without a write into
  the old inode, the second store of a key, the race of two writers with
  the other writer simulated between the write and the rename, an
  unwritable root); the compile step: a compile stores, a second make
  restores (no compiler, the stamp of this installation, the object the
  newest file, the compile clock counts the restore), a stale object is
  replaced from the cache, a miss on a changed source, on another digest
  and in another directory, the cache off, a forced recompile, an
  excluded module, an unwritable cache logged once, the first import
  under tiered from the cache, the lines of sprite-make (serial and
  --jobs 2).  unit_runner.py: test_cache_counts (the counts from the log,
  the note and the summary) and test_environment (the default of the
  cache, a value of the environment, the empty string).  Verified in this
  worktree through the runner at -j 1 on both backends (env -i, one file
  at a time, the sandbox of the agent on except for the functional tests,
  whose oracle writes under /tmp): the toolchain files (unit_cxx_toolchain,
  unit_product_cache, unit_runner, unit_make, unit_findcurry,
  unit_prebuild, unit_cxx_tiered, unit_conda, unit_plan, unit_cache,
  unit_loadsave, unit_cxx_bom) 24 of 24 pairs OK in 223 s after the fixes
  of the attempt (see below); the fast tier 54 + 78 pairs OK (173 s and
  260 s); the slow files (unit_examples, unit_equality, unit_configure,
  unit_curry2icurry, unit_currylib, unit_split, unit_py_fairness,
  func_flat2icurry, unit_benchmarks, unit_cxx_freevars,
  unit_cxx_gc_counters, unit_cxx_gc, unit_cxx_interp, unit_flat2icurry,
  unit_goals) 14 + 16 pairs OK (276 s and 185 s); the functional tests
  20 + 22 pairs OK (88 s and 46 s); make -C docs html built without a
  warning.  The attempt that the credits of the
  account interrupted left five failures, repaired here: the closure of
  the key did not see the Prelude unless the caller's path held the
  library (import_closure searches the library of the installation now;
  the first import under tiered missed its own entry because the import
  passes curry.path and the test passed the source directory alone); the
  store hard-linked the generated file (a link would carry the next
  generation into the entry; the .so alone is linked); the log capture of
  a test used an attribute the helper lacks; and the summary expectation
  of test_cache_counts counted two jobs of one directory as one.  Added
  here: the module name in the key (above), the real-path comparison of
  the stamp's text, the copy when a link is refused, the default of the
  runner and of cytest, and the tests of each.
- The review of the item (the same day), applied.  (1) The key missed
  the content of the ICurry and of the JSON: a JSON written again from
  the same source (after a change to the ICurry reader, a pinned .icy
  replaced, sprite-make --rewrite-flat) was served the old object; a
  probe showed the goal of a module give its old value after its JSON
  changed.  The chain digests the source, the .icy and the JSON of the
  module now, each one that exists (_module_texts), GENERATOR_SOURCES
  names the ICurry reader (icurry/, icurry/types/) and the loader and
  JSON writer of the toolchain, and KEY_FORMAT is 2 (the entries of
  format 1 are never found; the runner's prune removes them with the
  digest directory of the old stamp).  Tests: TestKey.test_texts_in_the_chain,
  TestCompile.test_miss_on_a_changed_json.  (2) Under interpret:new a
  warm cache placed the object of a module at the JSON stage, against
  section 4 of tests/README ("new ... never compiles it"): Cpp2So.restore
  places nothing under new now (test_interpret_new_does_not_restore); the
  mode keeps its documented meaning, and the owner may open it to the
  cache later (one line).  (3) The CI cache entry of tests/.cache carries
  the product cache, one digest directory per header change, never
  pruned: _productcache.prune(keep) removes the digest directories not
  kept and the leftovers of an interrupted store older than an hour,
  with nothing removed when the installation has no digest; the runner
  calls it after its prepare pass on its own cache alone
  (tests/.cache/products, not a directory the environment named:
  prepare.prune_product_cache, in the Python of the installation), and
  prints a line when something went.  The pass in CI with a warm cache
  is not measured (no CI run from this worktree); the entry is saved
  after every run as before.  (4) run_tests --changed selects
  unit_product_cache.py for a change to the toolchain (TOOLCHAIN of
  selection.py), and the manifest has its entry (--update-manifest, 28
  tests, 10.5 s on cxx, 0.5 s on py).  (5) scripts/new-worktree.sh
  copies the products of the front end alone now: the generated Python
  and its bytecode name the source of the main tree as the C++ did, and
  the Python backend writes them again in a moment.  (6) The owner's
  decision names the compiler and the format version in the digest, and
  both were missing: object_digest digests config.cxx_compiler(), the
  record of the build in sysconfig/cxx_compiler (a new .var under
  src/export/sysconfig: the version and the target of CXX_POSTINSTALL,
  "13.3.0 x86_64-linux-gnu" here; empty for an installation configured
  without a compiler), and compiler.FORMAT_VERSION.  The record, not a
  probe of the compiler at hand, so that an installation without a
  compiler (the conda package without cxx-compiler) computes the digest
  of its shipped objects; the record ships with the package.  The record
  is read once per process (a test that moves the prefix keeps the
  digest, as it should: two installations of one runtime give one
  digest).  TestFlavor.test_digest asserts both.  (7) The default of the
  cache (on, under the cache directory of the user) differs from the
  words of the issue ("optional; unset means today's behaviour"): left
  to the owner; the docs say how the directory grows and that it may be
  deleted.  Consequence of (6): every object compiled once more; make
  stage recompiled the library in 31 s, the prepare pass of the pool (43
  modules, cache cold for the new digest) took 44.6 s, the pass again
  0.5 s with every product present.  Verified through the runner under
  env -i (the conda CXXFLAGS of the login shell make the two
  precompiled-header tests of unit_cxx_toolchain.py fail by their member
  names, O3O2; not a defect of the change): unit_product_cache.py 28 OK
  on cxx and py, unit_cxx_toolchain.py 36 OK on both, unit_runner.py 67
  OK on both, unit_conda.py 21 OK on both.
- What remains.  The sharing between trees (above).  The conda build
  with the stamp change: not made; question 2 of conda/README.md names
  what it must show, and the build must now ship sysconfig/cxx_compiler
  (it does: every sysconfig file is installed).  The prepare pass in CI
  with a warm product cache: not measured.  The default of the cache
  (point 7 above).  A cache the environment names is never pruned; the
  directory may be deleted at any time.
- The suite stage (the same day), every run under env -i through the
  runner, one command at a time.  The cxx prepare pass of every corpus in
  one command, cache cold for the new digest: 456.2 s wall for 1,239
  modules (eqconstr 289.6 s, math 53.0 s, benchmarks 38.0 s), 1,197
  stored.  The py prepare pass over the corpora: 12.0 s, every product
  present.  The full suite at -j 2 --mem 4 on both backends in four
  chunks: unit_[a-c] 62 of 62 OK (236 s), unit_[d-o] 50 of 50 OK (175 s),
  unit_[p-z] 64 of 64 OK (121 s), func_* 42 of 42 OK (65 s, the sandbox
  off for the oracle); --changed selects every file (the harness changed),
  so these chunks are its run.  The five files of the item at -j 2 on
  both: 10 of 10 OK (112 s).  --fast on both: 130 of 130 OK (180 s).  The
  full suite at -j 1 on both backends, the long run that was left: 354 s,
  337 s, 320 s and 137 s, 218 of 218 OK, about 19 min.  The second
  worktree (scripts/new-worktree.sh, the diff applied to it, the cache
  warm): the make stage of its library stored 13, restored 0; its prepare
  pass of every corpus stored every module and took 752 s, longer than a
  cold pass here, because the copies of the front end's products kept the
  time of the originals (tar keeps it; cp -r, which the script used before
  this item, did not) and the sources of the fresh checkout were newer, so
  every module went through the front end again, and the module of smap
  that needs currypp failed under a PATH without PAKCS.  Fixed: the copy
  takes the time of the copy (tar -xm) and the JSON files are touched
  after it, so each is newer than its ICurry; the worktree made again
  showed the front end's copies newer than the sources and its make stage
  restored the 13 library objects of the earlier worktree of the same
  path (the key names the path, not the tree).  Its prepare pass then
  took 231.1 s with 652 of 1,239 from the product cache: the 587 others
  missed because the ICurry the front end wrote in the first worktree
  carries constrEq (the binding optimization rewrote the .fcy) while the
  ICurry copied from this tree, which came from the ICurry cache keyed by
  the source text, does not (sendMoreMoney.icy of this tree and of the
  main tree hold no constrEq; the two cache entries differ in eqInt
  against constrEq).  The product cache is right to tell them apart; the
  ICurry cache of the tests serving a pre-rewrite translation is a
  separate matter for the owner.  So the acceptance of #100 stays unmet
  for a worktree at a new path (a miss by construction, above), and a
  worktree made again at the same path is served.  The measurements left
  about 1,250 entries of the probe worktrees under tests/.cache/products
  (155 MB in all); the runner's prune keeps them because they share the
  digest; the directory may be deleted.

2026-10-07: products translated before the binding rewrite stay in use (#101, worktree wt/stale)
- Reproduced.  A pair as the route made it before the rewrite existed,
  the .fcy of the front end (curry2flat with rewrite=False) and its plain
  translation (flat2icy), both newer than the source, on a module with
  the guard f x | x == 3 = x and main = f y where y free: icurry_is_stale
  said False, currentfile returned the .icy, curry.import_ ran no front
  end and left the times, and main suspended (the rewritten program
  gives 3).  This worktree held two such pairs in its corpus, both
  extracted from the overlay archive by make overlay (tar -m gives them
  the time of the extraction, newer than the sources): atableNoflex (1
  equality) and poker_four_of_a_kind of smap (2); of the other 878 .fcy
  files under tests/data, 4 hold constrEq and the rest need no pass.  No
  file of the library (22) needs it.
- The cache, measured on the cache the main tree moved aside
  (tests/.cache/aside-prerewrite/icurry.db, 2,183 rows of format 3), the
  key of every corpus module computed under the digest of each era (the
  sources of flat2icurry at 1233abf8, 1b554fd1, aba8c864 and HEAD, with
  PORT_DIR mocked, and the Curry path of the drivers, the system library
  included): 1,239 rows are reachable under the digest of 1233abf8
  (10-04, before the pass), none with constrEq; 27 under 1b554fd1 (the
  in-memory pass; 2 with constrEq); 6 under aba8c864 and the same 6
  under HEAD (the digest has not changed since #85).  So the digest
  covered bindingopt.py from the day it joined PORT_DIR (frontend_digest
  digests every .py file there since 1233abf8), the rows of 2026-10-05
  were out of reach from 1b554fd1 on, and no hit served a pre-rewrite
  text after #85.  The pre-rewrite ICurry of the trees came from the
  .icy files on disk, which the plan counted current; the cache is asked
  only when the plan runs the step, so it was never asked.  What the key
  did not see is the step itself: the digest covers the sources of the
  pass, not the code that applies it (_frontend.curry2flat), so a change
  to the step alone (which files it rewrites, #99) left the key as it
  was.  The cache the main tree made on 10-07 after its refresh: 1,237
  of 1,441 rows reachable under HEAD, 7 with constrEq, the 7 modules the
  pass changes.
- The rule (_curry2icurry.translated_before_rewrite, asked by
  icurry_is_stale, which the plan asks of every .icy): an .icy is stale
  when the .fcy of the front end beside the source holds a Boolean
  equality the pass replaces and neither file holds constrEq.  The
  check reads the .fcy: constrEq in it means rewritten, no equality name
  means nothing to replace (both a fraction of a millisecond); else the
  pass runs over the text in memory and counts; the .icy is read only
  when the count is not zero, so an unrewritten .fcy the PAKCS oracle
  wrote beside an .icy of the rewritten program is current (the product
  is right; a stale verdict there would run the route at every import,
  since a hit of the ICurry cache writes the .icy and leaves the .fcy).
  Not judged: a .fcy older than the source (another version of the
  source, after a hit), and the modules of the library (committed
  ICurry, the owner's).  Cost: the Prelude 0.05 ms (a path comparison;
  its .fcy holds constrEq, the definition, so the first tier would
  answer for it too); the 893 .icy files under tests/data 0.74 s in
  all, 0.83 ms each (679 of the 880 .fcy files have an equality name
  without constrEq, so the parse runs for them: median 0.8 ms, mean
  0.9 ms, about 0.3 ms per KB, the largest 9 ms, assembler of kiel at
  28 KB).  Why not a marker time: a copy (tar
  -m in make overlay and in new-worktree.sh) gives a pre-rewrite pair a
  fresh time, so no time separates the two kinds of pair; the content
  does.  The product cache cannot bypass the verdict: Cpp2So.restore
  acts at the JSON and C++ stages alone, and its key digests the .icy
  text.
- The key: ROUTE_VERSION = 1 in _frontend.py, the version of the steps
  between the front end and the translation, in icurry_cache_key through
  cache.route_version(), not in frontend_digest: the product cache keys
  on that digest too and digests the ICurry text itself, so the version
  there would have recompiled every object once for nothing.  Every
  entry of the ICurry cache misses once after this change (one
  translation of each test module on the first run, as after the format
  change to 3); KEY_FORMAT stays 3, the row does not change.
- sprite-make prints "sprite-make: pre-rewrite pairs: N translated
  again" at the end of a run that made such a pair again (not under -q;
  the children of --jobs print it for the parent, which sums; the manual
  says so); the prepare pass reads the line (prepare.rewrite_counts) and
  repeats the count on the line of the directory and in its summary.
- Tests, both backends: tests/unit_curry2icurry_stale.py, 10 tests: the
  pair is stale and the import makes it again (the goal gives 3, the
  times move, a second make runs no front end); three current pairs,
  one per tier of the check, untouched by an import with the front end
  mocked away; the oracle's unrewritten file beside a rewritten .icy is
  current; a .fcy older than the source is not judged (the strict order
  of newer); the files not judged; the library left out; the cost (the
  best of five batches: the Prelude under 2 ms, a rewritten file under
  5 ms, a parse under 25 ms; measured 0.05, 0.1 and 1 ms); the version
  in the key and not in the digest; a version-0 entry misses, the route
  makes the pair again and stores the new entry, which a later miss is
  served from, and the old row stays; the lines of sprite-make (one
  pair, nothing on a second run and under -q, two children summed, the
  manual).  unit_runner.py: test_rewrite_counts (the line, the note, the
  summary).  tests/manifest.json has no entry for the new file.
- Verification in this worktree (env -i, one process at a time):
  unit_curry2icurry_stale.py 10 OK on cxx (4.2 s) and on py (4.3 s);
  through the runner at -j 2 --mem 4 --backend both: unit_curry2icurry,
  unit_curry2icurry_stale, unit_cache, unit_runner, unit_make,
  unit_product_cache, unit_flat2icurry_bindingopt, unit_findcurry and
  unit_plan 18 of 18 OK (74 s); func_flat2icurry and unit_cxx_toolchain
  4 of 4 OK (50 s; the oracle test over the products on disk is green
  with the refreshed pairs).  sprite-make --icy atableNoflex in the
  pool: the line with 1, the pair rewritten, a second run silent;
  poker_four_of_a_kind on a copy of smap without currypp on the PATH:
  the front end left its current files, the pass rewrote the .fcy, the
  .icy was made (the line with 1).  ./run_tests --prepare-only --backend
  cxx func_smap.py: "prepare: 48 modules in 2 directories on cxx, 1
  pre-rewrite pair translated again, every product present" (35 s, the
  pool compiled cold).  The docs were not rebuilt.
- The refresh.  A tree after an upgrade across the rewrite (products
  older than 2026-10-06, the products of the archive, copies of either)
  needs nothing by hand: the first import of a module makes its pair
  again; ./run_tests --prepare does the pool and the corpora in one go
  and says how many; sprite-make --rewrite-flat M does one module,
  current or not.  Documented in tests/README (sections 1, 8 and 10),
  AGENTS.md and the page CompilationPipeline.
- Left to the owner.  (1) A hit of the ICurry cache leaves the .fcy of
  the front end as it is, so the pair on disk disagrees until a hand
  rewrite (the oracle test accepts it); the route could run the pass
  over the .fcy on a hit too, a read and at most a parse.  A route
  change, not made.  (2) The gap of the rule: a source that names
  constrEq itself and has a required equality is judged current; none
  in the corpus.  (3) Option D of #99 (an .icy stale when the .fcy has
  constrEq and the .icy has not, after a hand rewrite of the file
  alone) is not covered; sprite-make --rewrite-flat covers that case.
- After the review (same day).  (1) The parse of the check recurred at
  every import of a current module in every process, and grew with the
  file (0.3 ms per KB, 1.2 s for a text of 840 KB).  Now the count of
  the pass is kept for the process by the path, size and time of the
  .fcy (one parse per module per process), and a .fcy larger than
  PARSE_LIMIT (256 KB, about 80 ms) is not judged, with one warning per
  process that names sprite-make --rewrite-flat.  (2) A pre-rewrite
  pair whose .fcy cannot be written again (a read-only file or
  directory) failed the import with a raw PermissionError from the
  pass; now it is not judged, with one warning per process, so the
  import uses the pair as before the rule.  (3) A hit of the ICurry
  cache on a flagged pair was counted as translated again; now it is
  counted apart (pairs_served), the line of sprite-make ends in
  ", M from the ICurry cache" when there was one, and the prepare pass
  repeats the clause.  (4) The TODO had justified the exclusion of the
  library with a parse of the Prelude that would not happen (its .fcy
  holds constrEq, the definition); corrected above, with the current
  example of the parse tier.  (5) tests/README section 1 now says that
  the rows of the old key stay in the table.  Tests, both backends:
  unit_curry2icurry_stale.py (14 now: the memo, the limit, the
  read-only pair, the served pair, the line with the clause) and
  unit_runner.py test_rewrite_counts.
- Suites over the whole diff (same day, after make overlay restored the
  two pre-rewrite pairs of the archive; -j 2 --mem 4, both backends):
  the prepare pass, 1,239 modules in 12 directories, "1 pre-rewrite pair
  translated again" (atableNoflex, both files rewritten) and "1
  pre-rewrite pair from the ICurry cache" (poker_four_of_a_kind: the
  .icy from the cache, the .fcy left as the front end wrote it); the
  eqconstr corpus, which the archive does not hold, compiled cold (767
  objects, 423 s).  ./run_tests --changed: 220 of 220 OK over two
  commands (the first was cut at 595 s with 189 run; the 31 files left
  ran after it); func_goal_defaulting failed in both backends inside the
  sandbox alone (PAKCS writes under /tmp) and passed outside it.  The
  named files unit_utility, unit_curry2icurry, unit_flat2icurry,
  unit_flat2icurry_bindingopt, func_flat2icurry, unit_cache, unit_runner
  and unit_curry2icurry_stale: 16 of 16 OK (133 s).  --fast: 132 of 132
  OK (210 s).  The oracle suites func_*.py outside the sandbox: 42 of
  42 OK (80 s).  No file of the suite failed because of the change.

2026-10-08: the nightly jobs green on a current baseline (worktree wt/nightly)
- The scheduled run of 2026-10-07 (d54c092b) failed in five jobs: the
  functional shard 2/3 and the functional suite of the Python backend
  (func_smap.py: poker_four_of_a_kind needs currypp, which the runner
  lacks; CI never extracted the overlay archive, so every scheduled run
  since 2026-10-03 failed there), interpret:off (unit_optimize_applies.py
  and unit_optimize_inline.py died with SIGSEGV), interpret:all
  (unit_cxx_heap.py, unit_cxx_passthrough.py and unit_benchmarks.py read
  an object that the mode never makes, or asserted interpret:off in the
  command of an item), and the stress job (unit_long_lists.py,
  unit_cxx_rotation.py, unit_examples.py and unit_benchmarks.py: the walk
  over the live state at every step, not a collector defect; right values
  up to every cut, no verifier abort).
- The CI item.  The four test jobs check out with fetch-depth 0 and run
  make overlay right after the stage (the products must be newer than the
  installed interfaces, else the front end compiles the module again and
  fails without currypp; overlay-prune is inert in a shallow clone).  The
  prepare pass names the cause when a module without a product names a
  preprocessor the PATH lacks and the log shows the failure; a failed pass
  keeps its meaning (the first version converted it to ok on the presence
  of the archived files; the review removed that).  unit_runner.py
  test_preprocessor_lacking; tests/README section 10.
- The interpreter item.  Under 'off' a module imported from an ICurry
  object has no step (materialize.py), so the value tests of the two
  optimizer files reload under 'new' for the unoptimized half
  (ModuleTestCase.interpretable of unit_optimize.py); TestInlineAllocation
  of unit_cxx_heap.py and TestStepFunctions of unit_cxx_passthrough.py
  skip under 'new' and 'all' (cytest.skipIfInterpreted); test_items of
  unit_benchmarks.py checks the commands in an environment without a
  mode.  tests/README section 4.
- The stress item.  unit_long_lists.py sorts two thousand elements under
  the variable on the C++ backend (ten thousand otherwise); the countQueens
  8 half of test_counters_of_a_search skips after its psort 8 assertions,
  and test_counters_repeat uses a board of six under stress (73967 steps,
  one rotation period); examples 20, 21 and 23 join STRESS_EXCLUDED
  (each unfinished after 580 s against about 2 s in the default mode).
  tests/README section 4 names every test that skips or shrinks there.
- The suite stage, same day.  Two more fixes of test assumptions: (1)
  test_split_suite of unit_benchmarks.py passes -e SPRITE_GC_STRESS=0 to
  the harness, whose child inherits the environment (QueensSet 8 whole,
  2/0 and 2/1 each exceeded the timeout of 120 s under stress; now 73
  tests in 39 s where CI took 421 s); (2) the guard no_front_end of
  unit_curry2icurry_stale.py (#101, newer than the failed nightly) raised
  on every command through _system.pexec, and under interpret:off the C++
  compile of the module goes through it; the guard now raises on the
  front end's command alone and runs the rest (2 failures under 'off'
  before; 14 ok under off, all and tiered after).
- Results, one runner at a time at -j 2 --mem 4 on the C++ backend after
  make overlay: the unit suite under interpret:off, 89 of 89 ok in three
  ranges (180, 65 and 95 s wall; the one failure, unit_curry2icurry_stale,
  fixed and rerun); under interpret:all, 89 of 89 ok (401, 84 and 145 s);
  the functional suites under interpret:all outside the sandbox, 21 of 21
  ok (133 s); under SPRITE_GC_STRESS=1, 89 of 89 ok (476, 126 and 206 s
  for the ranges, with unit_split.py alone at 466 s, unit_cxx_rotation.py
  205 s, unit_cxx_freevars.py 157 s, unit_typed_expr.py 151 s,
  unit_cxx_tiered.py 120 s); the default mode on both backends, --fast
  132 of 132 ok (199 s) and --changed 22 of 22 ok (145 s).  No CI run has
  confirmed the workflow change yet; the functional suites run on the
  schedule or a workflow_dispatch with functional=true.
- Left to the owner.  (1) The crash: under interpret:off a module
  imported from an ICurry object (curry.import_(imodule)) materializes
  its functions without a step, and the first evaluation dies with
  SIGSEGV in cyrtbindings generate_values (tiered and new give the
  value).  A guard at evaluation, a trap step or a check of info->step,
  would raise instead; a test under 'off' can follow it.  (2) The default
  goal of the root Makefile runs overlay before stage; a fresh clone
  built with plain make meets the currypp failure at the first use of
  poker_four_of_a_kind.  Swap the two, or document the order.  (3)
  overlay-prune could warn when git rev-parse --is-shallow-repository is
  true.  (4) make overlay prints about 5900 tar lines in every CI job.
  (5) A hit of the ICurry cache leaves the .fcy as the front end wrote it
  (the #101 entry).  (6) unit_split.py costs 466 s under stress on this
  host and about 470 s on the CI runner; a smaller board under the
  variable would cut the stress job by seven minutes.

2026-10-08: the reference flag of the Python backend, curry.save and the stepper on the C++ backend, and two findings of the audit (#82 stage 5; worktree wt/refflag)
- Stage 5, the flag.  The name does not change: backend:py stays the one
  mechanism, and the hiding is a matter of documentation and help texts.
  A second name that says "reference" would add an alias to the value set
  of the flag, to the help of sprite-exec -b, to the REPL, to the test
  runner, to the CI matrix, to the conda recipe and to the run scripts of
  the examples (7 test files name the flag in 24 lines and 36 lines
  compare its value; 12 lines under .github, 41 under examples, 5 under
  conda), for the month before the removal, which deletes every call site
  either way.  What changed: the user guide names no Python backend;
  docs/source/DeveloperNotes.rst (new, in the toctree after Developer
  Setup) names the flag through every surface (SPRITE_INTERPRETER_FLAGS,
  sprite-exec -b, python -m curry -b, :set backend, Interpreter(flags),
  curry.reload, configure --with-default-backend, run_tests --backend and
  run-tests.sh), what the backend is for (the oracle of the values of the
  C++ backend in the suite and in CI; the debugging aids: getimpl as
  Python source, PDB under the flag debug, the trace flag, the profiler of
  sprite-exec -p, the saved .py form with curry.load and sprite-make --py,
  the stepper cytest.step, the white-box tests), its limits (the speed and
  the baseline rows, recursion_limit, step_budget, no collector, #60, #62,
  the set-guard assertion, _biString, the repr of a negative Float) and
  the removal gate.  The sentences moved there from Quickstart (the
  selection and "Saving Compiled Curry", now the C++ forms), Examples (the
  choice paragraph and the "Backends:" lines; 03 and 04 "run on the
  reference backend"), MajorFeatures ("The Backend", one entry),
  CompilationPipeline (the Python target and the bytecode cache),
  sprite-exec ("The Backend", "Bounding One Alternative" with two flags,
  the stats fields, the profiler), sprite-make (the targets list, the
  section "Generating Python", the library sentence), EnvironmentVariables
  (the backend paragraph, step_budget, recursion_limit, the two collector
  examples, the rotation note), ConfiguringSprite (NAME for py|cxx), REPL
  (the option backend, the transcript), CurryPackage (the rows backend,
  interpret and rotation; the rows step_budget and recursion_limit go and
  the footnote points to the notes; curry.load; curry.backends),
  UsingTheAPI (getimpl, the info text, "Writing Compiled Code to Disk" in
  the C++ forms with the note on the registry of libraries, the string
  note, the limits), Architecture (one backend), Performance (the baseline
  rows), Status ("The backend", the limits, the epic list), and the
  phrases "both backends" of StepByStepBuild, Semantics, UsingTheAPI and
  Status.  The README of the repository, examples/README, the READMEs of
  examples 00 to 24 (the block "To pick a backend", the timings and the
  advice of the Python backend in the Limits sections; example 19 shows
  the plan as the C++ backend prints it) and the comment line of every run
  script follow; examples 03 and 04 keep backend:py in their run scripts
  and say that they run on the reference backend.  The help texts:
  sprite-exec -b shows BACKEND, not {py,cxx} (a metavar; the value is still
  checked), and the :set listing of the REPL says "The backend of the
  session" without the values; the docstring of the flag backend in
  curry.interpreter.flags names 'py' as the reference implementation and
  points to the notes.  Not changed: sprite-make --py and its help, the
  runner's --backend, run-tests.sh, ci.yml, the conda recipe and
  conda/README.md (the hygiene item owns the recipe tests), and the two
  source comments that name the backend (examples/03-inspect/go.py,
  examples/cxx/CxxType.curry).  unit_reference_backend.py (new): a grep
  over docs/source outside the two developer pages (DeveloperSetup names
  run_benchmarks -b py) and the generated pages, for the words and flags
  of the Python backend; the needles of the developer notes and the
  toctree; and the flag through Interpreter(flags), the environment,
  sprite-exec -b py with its help, and the :set listing.
- Stage 5, curry.save, curry.load and the stepper on the C++ backend.
  Probes: curry.save writes C++ source (format 12, an IMPORTS line); the
  step of the toolchain that sprite-make --so runs
  (backends.cxx.toolchain.Cpp2So) compiles it into a shared object when
  the file lies under the product directory of its source
  (_filenames.curryfilename refuses another path, "bad path for"); and
  curry.load reads the object.  For a module whose library is not loaded
  (hello of the pool, interpreted; a fresh module under interpret:new)
  the load returns the module in the same session, again after
  curry.reset(), and in a fresh Interpreter.  For a module loaded from its
  installed object (Data.Either) the loader refuses the file in all three
  cases ("it is already loaded from .../Data/.curry/sprite-pakcs-3.4.1/
  Either.so"): the runtime keeps one library per module name for the life
  of the process.  The saved program of the C++ backend is a stub:
  _generate_main writes an entry() that prints "Entry for 'M'" and exits.
  The stepper: cytest.step calls Evaluator.set_global_step_limit, which
  reads rts.stepcounter, and the C++ RuntimeState has none
  (AttributeError).  The C++ runtime counts the nested steps through
  procS (check_interrupts increments stepcount), so a step limit would be
  a field of the RuntimeState checked in check_interrupts with a
  terminate status that procD hands out: a change of the runtime,
  proposed, not made.  evaluator.single_step takes one step at the root on
  both backends (one step of goal gives f (g (g 0))).  Tests:
  unit_loadsave.py runs on both backends.  LoadSaveTestCase saves the four
  library modules in the form of the backend and loads the file: a known
  failure on the C++ backend (curry.load reads a .so), so its four tests
  are expected failures there.  TestRoundTrip (new) runs the round trip of
  each backend in a child: the .py form on the Python backend; on the C++
  backend the C++ source compiled by Cpp2So under the product directory
  and loaded with curry.load, in the session and after a reset, under
  interpret:new so that the fresh module stays interpreted (skipped
  without a C++ compiler).  TestSave of unit_goals.py runs on both
  backends: the four tests that run the saved program are expected
  failures on the C++ backend (SAVED_PROGRAM), test_library checks both
  forms (no moduleMain, no entry()), test_goal_from_scheme runs on both.
  test_interp_step of unit_py_runtime.py and test_apply_nf of
  unit_prelude.py are expected failures on the C++ backend instead of
  skips.  cytest.expectedFailureIf(condition, reason) is the decorator
  (tests/lib/cytest/__init__.py; a conditional expectedFailure, so a fix
  shows as an unexpected success).  The decision on a C++ form of
  curry.save (a main that evaluates the goal; a plan step that compiles a
  saved file) and on a step limit of the runtime stays with the owner;
  example 04 and the Python form follow it.
- Finding (a) of the audit: Interpreter(flags) started from the defaults.
  Probe: with SPRITE_INTERPRETER_FLAGS=backend:py,inline_budget:7 and
  SPRITE_ROTATION=steps:100 the global interpreter had py 7 steps:100 and
  Interpreter() had cxx 4 time:10ms.  The rule now: the flags of every
  interpreter start from the environment (flags.getflags: the defaults,
  SPRITE_ROTATION, SPRITE_INTERPRETER_FLAGS, the argument on top), as the
  global interpreter and curry.reload did already; Interpreter.__new__
  merges getflags(flags) over the defaults.  So a fresh interpreter of a
  test runs on the backend and in the mode of the session; test_04_cxx of
  unit_plan.py pins interpret:off, because under all the skeleton of the
  C++ backend ends at the JSON.  The docstrings of curry.interpreter.flags
  and of getflags state the rule; CurryPackage.rst and the note of
  UsingTheAPI.rst say that a new interpreter starts from the environment.
  TestFreshInterpreterFlags of unit_api.py pins it in a child: the
  environment is the base, the argument wins, an unknown flag is refused.
- Finding (b) of the audit: a cyrt Node compared with a Python value.
  Probe on the C++ backend: == and != against 1, True, None, object(),
  'a', 1.0 and [] raised TypeError (pybind11: incompatible function
  arguments), in both directions and in a containment test; on the Python
  backend == gave False for the unboxed primitives and raised TypeError
  ("not a Curry expression") for None, object() and [].  Fix: the bindings
  define __eq__ and __ne__ with py::is_operator(), so a failed cast returns
  NotImplemented and Python answers False and True; the Node of the Python
  backend returns NotImplemented for a value that is not a Curry
  expression.  Both backends answer False for == and True for != against
  every foreign object, in both directions and in a containment test.
  TestForeignObjects of unit_equality.py pins it on both backends.  Hash
  and ordering: neither backend defines an order (TypeError from Python
  itself, no gap); both hash by identity (Node::hash is std::hash of the
  pointer; the Python Node hashes id(self)), so two logically equal nodes
  hash differently and a set of two equal nodes has size 2 on both
  backends: a gap against the contract of __hash__, reported, not fixed
  (the test pins hash(node) == hash(node) alone).
- Results on this machine (the other item of the lane edits the same
  worktree; its files are not in these runs).  make stage after the
  change of the bindings: finished (the bindings relinked; the library
  objects stayed current).  Single files on the C++ backend:
  unit_equality 25 ok (2 skipped), unit_api 12, unit_plan 9, unit_loadsave
  5 (4 expected failures), unit_goals 48 (4 expected failures),
  unit_py_runtime 12 (7 skipped, 2 expected failures), unit_prelude 17 (1
  expected failure), unit_importer 2, unit_reference_backend 6.  On the
  Python backend: the same files with unit_py_conversions 10 and
  unit_py_interp 6, all ok (unit_goals 1 skipped, unit_py_runtime 1
  skipped and 1 expected failure, unit_prelude 1 skipped).  Under
  interpret:all: unit_loadsave, unit_plan, unit_api, unit_equality,
  unit_goals, unit_importer, unit_cxx_runtime (5) and
  unit_reference_backend, all ok; under interpret:off: unit_loadsave,
  unit_plan, unit_goals, unit_api, unit_equality, unit_py_runtime and
  unit_prelude, all ok; under SPRITE_GC_STRESS=1: unit_equality,
  unit_loadsave, unit_prelude and unit_api, all ok.  The runner at -j 2
  --mem 4 --backend both over 22 files (the changed files, the fresh
  interpreters of unit_make, unit_cxx_rotation and unit_prebuild,
  unit_inspect, unit_cxx_interp, the py-only files and the expected
  failures of the Python backend): 44 of 44 run, 1 failed, wall 142.7 s;
  the failure was unit_inspect.py on cxx, testGetICurry and testGetImpl
  with Peano interpreted, the fresh worktree without the objects of the
  pool; after --prepare (43 modules, 26 s) 27 of 27 ok.  unit_examples.py
  on both backends: 22 and 22 ok, 136.6 s on py and 26.5 s on cxx.  The
  unit suite on both backends in two halves at -j 2 --mem 4: unit_c*.py 60
  of 60 run, 3 failed, wall 249.8 s, the failures unit_cxx_heap (2),
  unit_cxx_passthrough (1) and unit_cxx_variable (1), each on cxx for the
  missing Tak1.so of the benchmarks corpus in the fresh worktree, and 0
  failed after --prepare (40 s for the corpus); the other 89 files, 120 of
  120 run, 0 failed, wall 339.5 s.  make -C docs html: status 0, no
  WARNING line in the log, DeveloperNotes.html rendered.  Not run here:
  the functional suites, and the unit suite under interpret:all, off and
  the stress mode as whole runs (the single files above cover the changed
  tests under each).
- Left to the owner.  (1) The C++ form of curry.save: a main that
  evaluates the goal and a step that compiles a saved file, or the
  retirement of the saved program with the Python form; example 04 and
  the Quickstart follow.  (2) A step limit of the C++ RuntimeState for the
  stepper, or the acceptance of evaluator.single_step and the bytecode of
  getimpl as the aids; the two expected failures then flip.  (3) The hash
  of a node is by identity on both backends while equality is structural.
  (4) Example 03 could run on the C++ backend under interpret:off: the
  first line of getimpl is the banner /****** Nat.natToInt ******/ of the
  generated C++, stable across compilers; under tiered the fresh module is
  interpreted and the first line counts bytecode units.  (5) The
  developer notes name the sentences the user guide dropped; the second
  half of stage 4 and stage 6 remove the notes' section with the backend.
- Review follow-up (four findings).  (1) The generated manual of
  sprite-make (sprite-make-man.rst and -usage.rst, written from -h and
  --man by the docs build) still named the Python target: "-p, --py,
  --python make Python files", "-g in --python mode", the paragraph on the
  Python target with its example, and "--py and --so ... for both
  backends".  Now the help of -p,--py,--python is argparse.SUPPRESS (the
  option works as before: make stage, the runner, unit_make and example 04
  use it), -g says "specifies the goal of a generated program", the manual
  names the C++ targets and --so for the library, and the three error
  messages of the target checks name no --py.  The developer notes name
  the hidden option.  (2) CompilationPipeline.rst said the library is
  compiled "for both backends ... with sprite-make --py and --so" over a
  line break: now "into the installation with sprite-make --so".  (3)
  Examples.rst and the READMEs of 03 and 04 taught the Python form
  ("Compile a Curry module to a Python script", "generated Python",
  "Python script"): the section of 04 is "Save a compiled Curry module",
  both say "the saved form of the reference backend" and point to the
  developer notes, and the C++ form stays the open decision above.  (4)
  The errors printed the hidden value: sprite-exec -b foo said "invalid
  choice: 'foo' (choose from 'py', 'cxx')" and the REPL said "Expected one
  of 'py', 'cxx'".  Now -b takes a type function (backend_name in
  curry.__main__) that says "unknown backend 'foo'", and the REPL says
  "Invalid backend: 'foo'." alone; the developer notes say so.  The test
  unit_reference_backend.py grows: the grep matches a phrase over a line
  break (a space in a pattern matches white space), which found two more
  sentences ("The Python backend does not use it" of SPRITE_GC_REPORT in
  EnvironmentVariables.rst and the note of UsingTheAPI.rst, both dropped);
  the patterns add --py, --python, "both backends", "Python form",
  "generated Python", "Python mode" and "Python target";
  test_sprite_make_option pins the help and the manual of sprite-make
  against the patterns, the error of a missing target, and that -p, --py
  and --python still parse (with -S); test_sprite_exec_option and
  test_repl_option pin the two error texts.  unit_make.py keeps its
  assertion "at most one of" on the third message.
- Suite stage, after make stage and make overlay (the prune removed 209
  products of the changed sources; ./run_tests --prepare-only on both
  backends compiled them again in 422 s).  ./run_tests --changed selects
  the whole suite (199 of the file-backend pairs: the Makefile and the
  tool sources count as unknown paths).  Through the runner at -j 2
  --mem 4 on both backends: the 14 new and changed test files 28 of 28
  ok (137 s); --fast 134 of 134 ok (183 s); func_*.py 42 of 42 ok
  (99 s); unit_[a-b]*.py and unit_[d-z]*.py 120 of 120 ok (343 s);
  unit_c*.py 60 of 60 ok (195 s, after one run with the conda toolchain's
  CXXFLAGS in the environment, under which the two header tests of
  unit_cxx_toolchain.py see a member named O3O2-...: an effect of the
  shell, not of the diff; the file passes in a clean environment and
  under env -i through the runner).  On the C++ backend, whole runs of
  unit_*.py: interpret:all 90 of 90 ok (659 s), interpret:off 90 of 90
  ok (310 s), SPRITE_GC_STRESS=1 90 of 90 ok (737 s).  unit_split.py
  alone under stress 4 ok in 14.7 s.  make -C docs html: status 0, no
  WARNING line; no page of the user guide outside the two developer
  pages and the generated listings names the Python backend.

2026-10-08: the build and CI hygiene items of the nightly lane (worktree wt/refflag)
- The default goal of the root Makefile runs the stage and then the
  overlay, the order of the CI jobs.  The order is load-bearing: a probe
  with the sources of the smap corpus copied to a scratch directory and
  dated 2001, their archived products dated 2002, and the interfaces of
  the installation dated today ran the front end at the import of
  poker_four_of_a_kind (the command of curry-frontend in the debug log,
  then "currypp: not found"); with the products dated after the
  installation, the module imported from them.  The comment at the goal
  says why; scripts/new-worktree.sh copied the products after its make
  stage already, and its header names the reason.  test_default_goal_order
  of unit_conda.py dry-runs the goal.  The step-by-step build page
  (docs/source/Installation/StepByStepBuild.rst) listed the overlay as
  step 2, before the stage; the overlay is step 4 now, after the stage,
  with the reason.
- make overlay lists the members it extracts under V=1 alone
  (OVERLAY_TAR_VERBOSE; make help names the variable).  A run in a clone
  printed 5946 lines before (tar xvzf) and prints seven lines of make now.
  test_overlay_rules of unit_conda.py pins tar xzf by default and tar
  xvzf under V=1.
- overlay-prune warns in a shallow clone whose history ends before the
  commit that packed the archive: git log -1 names the last commit of
  the history (the boundary commit shows every file as added), the diff
  from it is empty, and nothing is pruned.  The rule checks git
  rev-parse --is-shallow-repository and whether the parent of that
  commit resolves, so a shallow clone that reaches the commit prunes as
  a full clone does, without the warning.  The warning names fetch-depth
  0 and git fetch --unshallow.  test_overlay_in_a_shallow_clone of
  unit_conda.py clones the tree at depth 1 under TMPDIR (file://), copies
  the build files of the working tree in, runs make overlay once (the
  copies of the interfaces take most of its 14 s), and checks the
  warning, the quiet output and the product of poker_four_of_a_kind.
  tests/README section 10 says the same.  Review: the rule cannot tell a
  history that ends before the packing commit from one that ends at it
  (a clone with --shallow-since at the committer date of 64f715d1 names
  64f715d1 for the archive, its parent does not resolve, and the run
  printed the warning and the five rm lines of CxxGc that a full clone
  prints), so the sentence is conditional now: "its history ends at
  COMMIT, the commit git names for the archive.  If the archive was
  packed earlier, the products of the sources that changed since then
  are not pruned."  The Makefile comment says why.  Checked again in a
  depth-1 clone (the warning, no rm line) and in the boundary clone (the
  warning, five rm lines).
- unit_split.py checks the solutions of the split programs at a size of
  five under SPRITE_GC_STRESS=1 on the C++ backend (six otherwise; five
  on the Python backend as before); the permutations stay at five.  The
  profile under stress at six, one process alone on this host:
  test_searchqueens 323 s (whole 6 82 s, queens 6 84 s, the parts 82, 45
  and 30 s for k = 2, 4 and 8; the permutations at five about 1 s),
  test_queensset 21.6 s, test_permsort 0.4 s; the file 400.8 s.  At
  five: SearchQueens 12.9 s, QueensSet 1.4 s, PermSort 0.1 s; the file
  13.5 s.  The default mode is unchanged: the file 0.9 s on cxx (3.1 s
  at the first run, which compiled the split corpus) and 6.9 s on py
  (11.6 s at the first run, which wrote the Python products), and the
  queens programs give 10 solutions at five.  tests/README section 4
  names the file in the list of the stress mode.
- Results, one process at a time: unit_split.py under stress 4 ok in
  13.5 s; unit_conda.py 23 ok in 36 to 42 s; unit_configure.py 17 ok in
  10 s;
  the three files through run_tests -j 2 --mem 4 --backend both, 6 of 6
  ok, wall 69 s.  ci.yml parses.  No CI run has confirmed the quiet log
  or the warning yet.
- Review follow-ups.  scripts/setup-dev-machine.sh runs make overlay
  after make stage in its step 7, in the clean environment: the PAKCS
  distribution it installs has no currypp, so the first test run of a
  new machine made poker_four_of_a_kind through the front end and
  failed.  test_plan of unit_setup_script.py pins the order (a file of
  the other item, reported), and the Developer Setup page names the step
  (also reported).  The comment of unit_split.py says 15 s for the
  stress mode at five (measured 13.5 to 16.4 s; it said 20 s).  The
  manifest rows of unit_conda.py and unit_split.py were refreshed with
  ./run_tests -j 1 --backend both --update-manifest while the other
  item ran its tests on the same host: unit_conda.py 33 s cxx and 36 s
  py with 23 tests (8 and 7 s with 11 before), unit_split.py 13 s on py
  (7.5 s before; 1 s on cxx as before).
- Suite stage: make stage (7.7 s, nothing to rebuild) and then make
  overlay (11.2 s; 209 rm lines of the products whose sources changed
  since the archive) in the order of the default goal; unit_conda.py 23
  ok and unit_configure.py 17 ok on both backends through the runner;
  unit_split.py 4 ok on both backends and 4 ok in 14.7 s alone under
  SPRITE_GC_STRESS=1; the whole unit suite under the stress mode 90 of
  90 ok in 737 s.  The results of the whole suite are in the entry
  above.

2026-10-08: compiled objects portable between trees (format 13; worktree wt/objfmt; the decision of 2026-10-08 under issue #100)
- The decision.  An object of the C++ backend names no path, so an object
  compiled in one tree serves another: a copy of the tree at another
  path, a relocated installation, and the product cache of a second
  worktree.  The link line, the stamp, the cache key and the loader
  changed; the runtime (node layout, info tables, bom.hpp) did not.
- The SONAME.  Every object carries the SONAME "sprite-<module>.so.<format>"
  (toolchain.soname; module_of_soname is the inverse): the full module
  name, dots included, and compiler.FORMAT_VERSION.  The first form tried,
  "<module>.so.<format>", matched the names of the system libraries
  (libgcc_s.so.1 parsed as the module libgcc_s, and the stage failed with
  "Curry module 'libgcc_s' not found"); the prefix holds a character no
  module name holds, so the two kinds of name never meet.  The compile
  step reads the module name from a new header line of the generated file,
  "// MODULE: N", after the IMPORTS and FORMAT lines (vEmitHeader; a file
  of the current format without the line is a PrerequisiteError), and
  passes -Wl,-soname.  The objects of the imports stand on the link line
  by their files as before; the linker records their SONAMEs as the NEEDED
  entries, so readelf -d of every installed object shows no path
  (sprite-Prelude.so.13, sprite-Data.Maybe.so.13, libcyrt.so and the
  system libraries).  The link line pins -Wl,--no-as-needed (LINK_FLAGS,
  after the review below), so the NEEDED entries name every import of the
  module in the order of the IMPORTS line, whatever the default of the
  toolchain; the imports of a module stay in its record as well.
- The loader.  The dynamic linker satisfies a NEEDED entry with an object
  of that SONAME the process has mapped, whatever its path, RTLD_LOCAL or
  RTLD_GLOBAL, and no search path leads to the file (the probe: two small
  objects outside Sprite, A.so with a SONAME and B.so linked against it;
  B alone fails with "ProbeA.so.13: cannot open shared object file", A
  then B gives 42 under both modes, A from another directory then B too,
  and a directory on LD_LIBRARY_PATH that holds a link named like the
  SONAME serves as well; glibc 2.39).  So loader.load_module reads the
  NEEDED entries of the object before it opens it (backends/cxx/elf.py, a
  reader of the dynamic section of a 64-bit ELF file, checked against
  readelf in the tests), imports the modules they name
  (loader.needed_modules), and raises DynloadError when one of them runs
  without an object ("its import X runs without a compiled object")
  instead of the message of the dynamic linker.  The plan path imported
  them already (Cpp2So.import_lacks_an_object, from the IMPORTS line);
  the loader's own pass covers curry.load and an object without its
  generated file.  The tiered swap (cyrt/tiered.cpp) is unchanged: a job
  runs after the jobs of its imports and the swap maps the import's
  object before the importer's; a failed import swap now leaves the
  importer interpreted with "cannot open shared object file" in the log,
  where the path-based entry mapped the import's file from disk.  A
  program that dlopens an object without the loader must open the objects
  of its imports first, in dependency order, with libcyrt.so loaded or on
  the search path, or name on LD_LIBRARY_PATH a directory that holds each
  import under its SONAME (Cpp2So documents it).  bin/sprite-invoke
  exports LD_LIBRARY_PATH=$SPRITE_HOME/lib, which is why the extension
  module and the objects find libcyrt.so by name; a test of a relocated
  copy must set the variable to the copy, as the launcher of the copy
  would.
- The record.  compiler._source_file_name names a source under the
  installation relative to it, as before, and any other source relative
  to the directory where the toolchain places the object of that source,
  "../../M.curry" (the product directory beside the source).  The reader,
  loader.source_file, tells the forms apart by the prefix: a record that
  begins with "../" or "./" is relative to the directory of the object,
  another relative record is relative to the installation, an absolute
  record is a path.  The C++ side stores the string and reads it nowhere
  (dynload.cpp); the one other reader, unit_cxx_bom.py, checks the suffix.
  A module from text (curry.compile, str2module) keeps its temporary
  directory in the metadata entry all.tmpd, as before; such a module is
  never cached.  The record of the Python backend is unchanged (an
  absolute path for a source outside the installation): the backend has
  no objects and writes its files again in a moment.
- The format and the key.  compiler.FORMAT_VERSION is 13; the digest of
  the stamp covers it (object_digest, "format %d"; TestFlavor.test_digest
  asserts it), so every object was compiled once more: make stage
  recompiled the 13 library objects, and the prepare pass of the pool (43
  modules) took 35.5 s cold.  Cpp2So.product_facts dropped 'installation'
  and 'source'; nothing else in the key names a tree (the generator digest
  and cache.frontend_digest digest contents, 'subdir' is a name, the chain
  digests the texts of the module and of its imports, and the .icy and
  the JSON of the library hold no path), and _productcache.KEY_FORMAT is 3
  (the parts of the key changed).  Measured: a second tree made of the
  pool (tests/data/curry copied without its C++ products, under the build
  root, outside TMPDIR, which the cache excludes) with the cache of the
  runner: sprite-make -k -c -z --so over the 43 modules in 0.7 s,
  "product cache: 43 restored, 0 stored", the restored object one inode
  with the first tree's, its record "../../CxxBom.curry", and CxxBom
  imported from the copy in a child with the compiler counted: the value
  9, no command, compile 0.0 s.  The first attempt under TMPDIR compiled
  all 43 in 32 s: _productcache.excluded leaves the temporary directory
  of the system out, by design.
- Tests.  unit_cxx_toolchain.py (42 on cxx; the TestPortableObjects
  class): the SONAME form and the names that are not one; the SONAME and
  the NEEDED entries of a compiled module, read by elf.read_dynamic and
  checked against readelf, no path in any entry, the MODULE line and the
  record "../../M.curry" in the generated file, no path of the tree in it,
  and the module object that names the source beside the product
  directory; the two forms of the record and their resolution; every
  current object of the installed library (SONAME of its module, no path,
  Data.List names sprite-Prelude.so.13); a copy of a tree with its
  products at another path, the stamp rewritten through a build prefix as
  conda would, imported in a child with the compiler counted (no command,
  the value, the source and the object of the copy); curry.load of an
  object whose import is not loaded (the import comes from its object
  first, under interpret:new) and the error when the import runs without
  an object.  The hand-written JSON modules of those tests call a
  recursive function (Prelude.foldr, or themselves) so that the object
  takes a symbol of the import; the inliner would fold a call of a small
  function away, and --as-needed would drop the library.
  TestFormatStamp follows the new header line.  unit_product_cache.py (28
  on both): the facts without the two paths and without any path,
  KEY_FORMAT 3, and test_hit_in_another_tree in place of
  test_miss_in_another_directory (the same key in both trees, a restore
  without a compiler, one inode with the first tree, the module run in
  the second tree in a child).  unit_conda.py (24 on both):
  test_generated_files_name_no_prefix expects the record "../../CondaProbe.curry"
  on cxx, and test_relocated_installation_loads_its_objects copies the
  staged installation (without the precompiled header, the bytecode
  caches and the static archive; links as links), rewrites the 13 stamps
  as conda would, and runs a child under the copy (SPRITE_HOME,
  PYTHONPATH and LD_LIBRARY_PATH of the copy): the Prelude and Data.List
  load from the objects of the copy, no command runs, compile 0.0 s, no
  object of the original is mapped (/proc/self/maps), and no file of the
  copy changes.  tests/manifest.json rows of the three files refreshed
  (the counts changed) while the other lane ran on the host.
- Results, env -i, one process at a time: unit_cxx_toolchain.py 42 OK
  (34 s), unit_product_cache.py 28 OK, unit_conda.py 24 OK, unit_loadsave.py
  5 OK, unit_cxx_tiered.py 20 OK, unit_cxx_bom.py 10 OK, unit_cxx_emit.py
  5 OK, unit_cxx_partial.py 18 OK on cxx (the last two files found their
  pool modules interpreted until the prepare pass of the pool ran, as
  section 4 of tests/README says); unit_cxx_bom.py and unit_loadsave.py
  OK under interpret:new.  Through the runner at -j 2 --mem 4: the six
  files of the item on both backends 12 of 12 OK (84 s); 'unit_cxx_*.py'
  on cxx 20 of 20 OK (113 s); the toolchain and loader files (unit_cache,
  unit_compile*, unit_curry2icurry*, unit_make, unit_findcurry,
  unit_prebuild, unit_plan, unit_currylib, unit_loadsave, unit_inspect,
  unit_api) on both 28 of 28 OK (194 s; unit_currylib remade the library
  under the new format).  make -C docs html built without a warning.
  --changed selects everything (scripts/new-worktree.sh is an unknown
  path); the full suite is the long run of the suite stage.
- Docs and notes.  conda/README.md: step 9, "Relocation", open question 2
  (the stamp and the objects closed; open: the split) and "What remains";
  the next package build must show 13 text entries and no binary entry in
  info/has_prefix, readelf -d of Data/List.so with sprite-Prelude.so.13
  and no path, the first import of the Prelude from the shipped object,
  sprite-make --so of a program without a recompile of the library, and
  no file of the package changed after the runs.  meta.yaml keeps
  detect_binary_files_with_prefix spelled out so that a binary file that
  holds the prefix again shows up.  EnvironmentVariables.rst (the product
  cache serves every tree of one runtime; the sentence that said the
  cache is on by default is gone, config says off), CompilationPipeline.rst,
  sprite-make.rst, tests/README sections 1 and 10, scripts/new-worktree.sh.
- What remains, for the owner.  (1) The second line of the stamp is now
  the one tie between an object and an installation: the object names
  libcyrt.so and its imports by name, so an object compiled under another
  installation of the same runtime (same digest) would load; the line
  keeps a copy made by hand stale, as decided under #100.  Dropping it
  would make a hand copy keep its objects too and remove the one
  has_prefix entry of the package.  (2) The record of the Python backend
  still names a source outside the installation by absolute path.  (3)
  The conda build with the two changes has not been made.  (4) A failed
  swap of an import under tiered leaves the importer with the message of
  the dynamic linker in its log (above); the swap could map the imports'
  objects itself, a change in tiered.cpp.  (5) The hand-written JSON
  modules of the tests are typed loosely (foldr over three literals, never
  evaluated).
- Review of the item (the same day), five findings.  (a) An object
  without its generated file (sprite-make --tidy removes the .cpp) whose
  import lost its object: the plan trusted the object and the loader
  raised DynloadError for the interpreted import.  Now
  Cpp2So.import_lacks_an_object reads the imports from the NEEDED entries
  of the object (loader.needed_modules) when the file is gone, so the
  importer is interpreted as with the file; the DynloadError stays for
  curry.load alone.  (b) The NEEDED entries followed the --as-needed
  default of the toolchain (g++ of Debian and Ubuntu pass it; conda
  compilers and other distributions do not): LINK_FLAGS pins
  -Wl,--no-as-needed, so the entries are the imports of the IMPORTS line
  in order on every toolchain, and the loader's pre-import covers every
  import of an object without its file.  The flag enters the digest, so
  make stage relinked the 13 library objects once more (43 s); every one
  names each of its imports now (Data.Function names sprite-Prelude.so.13,
  Control.Monad names libc.so.6).  (c) elf.read_dynamic read the dynamic
  section through the section headers and returned an empty result for
  an object without them; it finds the section through the program
  headers (PT_DYNAMIC) and the string table through the PT_LOAD that maps
  it, as the dynamic linker does, reads with seeks, and raises ValueError
  for a file without program headers or without a dynamic section (a
  relocatable object).  Checked against readelf on an installed object
  and on a copy after strip --strip-section-headers (binutils 2.42).  (d)
  An object-relative record counts on the layout of the toolchain; an
  object compiled elsewhere (a file of curry.save compiled in another
  directory, as TestRoundTrip does not) resolved it to a file that does
  not exist ("/M.curry" for /out/M.so).  loader.source_file now resolves
  such a record only for an object under a product directory
  (_filenames.curryfilename) and gives None otherwise, so the module has
  no source file rather than a wrong one; curry.save says where to
  compile the file.  (e) A docstring of 118 columns in the tests, wrapped.
  Tests: unit_cxx_toolchain.py (46 on cxx) test_needed_entries_are_the_imports
  (a module that imports a module and never calls it names it), the
  installed library check that the NEEDED modules equal the IMPORTS line,
  test_object_without_its_generated_file (both .cpp files removed: the
  modules load from their objects with nothing compiled; the import's
  stamp removed: both interpreted under interpret:new, no error),
  test_elf_reader (the stripped copy, the file without program headers,
  a file that is not ELF), test_object_compiled_elsewhere_has_no_source
  (the object in another directory loads and __file__ is None; the
  product cache off in the child, as the step keys by the source of a
  product directory), test_record_forms with the records of an object
  outside a product directory.  Through the runner at -j 2 --mem 4 on
  both backends: unit_cxx_toolchain.py, unit_product_cache.py,
  unit_cxx_tiered.py, unit_loadsave.py, unit_conda.py 10 of 10 OK
  (115 s).
- The suite stage (the same day), every run under env -i through the
  runner at -j 2 --mem 4 unless said, one command at a time, the other
  lane on the host.  make stage found the install current (7 s) and make
  overlay took 9 s.  The cxx prepare pass of every corpus with the
  product cache of the runner, cold for format 13 under the link flags
  of the review: 578.4 s wall for the 1,239 modules of the 12 directories
  (eqconstr 352.2 s, math 84.6 s, benchmarks 40.5 s), 38 from the
  product cache (the pool after its relink), one pre-rewrite pair
  translated again, every product present.  The six files of the item
  (unit_cxx_toolchain, unit_product_cache, unit_cxx_tiered, unit_loadsave,
  unit_conda, unit_examples) on both backends: 12 of 12 OK (216.8 s).
  --fast on both: 134 of 134 OK (208.4 s).  --changed selects every file,
  so the full suite ran in four chunks on both backends: unit_[a-c] 64 of
  64 OK (233.7 s), unit_[d-o] 50 of 50 OK (162.2 s), unit_[p-z] 66 of 66
  OK (160.8 s), func_* 42 of 42 OK (107.9 s, the sandbox off for the
  oracle); 356 of 356 in all.  The unit suite under
  SPRITE_INTERPRETER_FLAGS=backend:cxx,interpret:all on cxx in three
  ranges: 32 of 32 OK (405.5 s), 25 of 25 OK (107.6 s), 33 of 33 OK
  (132.1 s).
- The acceptance of #100, measured (the same day).  A second worktree
  made with scripts/new-worktree.sh from this branch at another path,
  with SPRITE_PRODUCT_CACHE naming the cache of the runner of this tree
  (warm from the prepare pass above): the script, from the committed
  base, took 66 s (the stage of its library stored 13 under the digest of
  the base); the uncommitted diff applied (git diff | git apply, elf.py
  copied) and make stage again: 37 s, "0 restored, 13 stored", because
  the library of this tree was compiled by make stage without the
  variable and so never entered the cache, not because the keys differ:
  Cpp2So.product_key of the Prelude is one and the same string in both
  trees (the facts: format 13, the generator digest, the optimizers, the
  inline budget, the subdir, the frontend digest).  Its prepare pass of
  every corpus on cxx: 17.0 s wall, 1,240 of the 1,239 modules from the
  product cache, every directory "N of N from the product cache", nothing
  compiled (the line of kiel says 33 of 32: one more restore than modules
  listed, a module an import of the corpus names).  So the acceptance of
  #100, the prepare pass of a second worktree in under a minute with a
  warm cache, is met: 17 s against 578 s cold here and 231 s for the
  partial hits of 2026-10-07.  The restored CxxBom.so of the second tree
  is one inode with the object of this tree, its NEEDED entries are the
  SONAMEs and the system libraries, its generated file holds the record
  "../../CxxBom.curry" and no path of either tree; CxxBom imported in a
  child of each tree with the compiler counted gave 9 with no command,
  and /proc/self/maps of each child showed no object of the other tree.
  The worktree, its branch and its build root were removed afterwards;
  the 13 entries of the base's digest and the 13 library entries of this
  format stay in tests/.cache/products.

2026-10-08: the route on a cache hit, example 03 on the C++ backend, the hash rule and the note on the saved program (decisions 4 and 6 of the action list; worktree wt/potholes3)
- Decision 4, the route on a hit.  A hit of the ICurry cache wrote the
  .icy and the interfaces and left the .fcy of the front end as it was,
  so a pre-rewrite pair made again (make overlay over a tree whose cache
  held the module) disagreed until a hand rewrite (#101, left to the
  owner).  Now Curry2ICurryConverter.convert runs the binding
  optimization over the .fcy on a hit as well
  (_curry2icurry.rewrite_on_hit): a read; a parse when the text has an
  equality name (the exact test of "nothing to replace"; a rewritten
  file parses too, as after a miss, so the gap of a source that names
  constrEq itself is not widened); the file is written when the pass
  replaces an equality, else it keeps its bytes and its time.  Left
  alone: a .fcy older than the source (another version of it; a rewrite
  would make it current by the times for the front end, which would
  then skip the module), a missing file, and a file the pass cannot
  write again (a read-only directory), the last with one warning per
  process that names sprite-make --rewrite-flat; the hit stands in
  every case.  ROUTE_VERSION stays 1: the ICurry text of an entry does
  not change, so no entry misses.  The count of served pairs stays as a
  report (pairs_served; the line of sprite-make and the prepare pass as
  before), with its meaning corrected in the docstrings, the manual of
  sprite-make, tests/README and the page CompilationPipeline.
- The oracle test.  func_flat2icurry.test_products_on_disk accepted an
  unrewritten .fcy beside a rewritten .icy for every pair
  (accept_unrewritten).  Now the acceptance is narrowed to the one
  writer Sprite does not control, the PAKCS oracle, by its products
  beside the file: the Prolog of :load (M.pl) and the AbstractCurry of
  :eval (M.acy) (oracle_wrote, check_on_disk, ORACLE_SUFFIXES).  A pair
  without them that differs fails the test with the usual report, since
  the routes leave no such file now; the docstring says so.  On this
  tree no pair needed the acceptance (the test prints the list when one
  does).  The 15 modules with M.pl and M.acy under tests/data are
  copies of the products of the main tree (the time of the extraction),
  the oracle's hand there; the archive packs neither.
- Tests, both backends.  unit_curry2icurry_stale.py, 18 (14 before):
  TestCacheHit, a hit on a module whose .fcy needs the rewrite leaves
  both files with constrEq and the .fcy byte-identical to the file a
  translation writes (the program gives 3; a second rewrite_on_hit
  replaces nothing); a hit on a module whose .fcy needs none leaves its
  bytes and its time (no parse without an equality name, one parse with
  one outside a required position); a .fcy older than the source and a
  missing file are not judged (None); a read-only directory: one
  warning, the hit stands, the program runs.
  test_served_pair_is_counted_apart and the cached run of
  test_sprite_make_reports now expect constrEq in the .fcy too.
  func_flat2icurry.TestUnrewrittenFile: the pair without a product of
  the oracle is DIFFERENT through check_on_disk, and EQUAL with the mark
  unrewritten beside an empty M.pl or M.acy.
- Decision 6d, example 03.  The run script no longer selects
  backend:py: it keeps the flags of the environment (backend:cxx by
  default) and appends interpret:off (a repeated flag parses, the last
  wins), so the module is compiled before it runs and inspect.getimpl
  reads the generated C++, which the README describes; the first line
  is the banner /****** Nat.natToInt ******/ under every mode (the text
  of an interpreted function starts with the same banner, and its second
  line counts bytecode units), so the standard output of the example
  does not depend on the mode, and the code on the standard error stream
  does.  go.py prints "The
  C++ implementation ..." and the banner; the README says why and gives
  the timings (warm 0.3 s; the first run about 2 s more, the front end
  and g++); expected.out regenerated from the run.  Under backend:py
  the script prints the signature of the generated Python function
  instead, so the output is not identical and unit_examples.py lists
  the example on the C++ backend alone, with the reason beside the
  entry.  Examples.rst and examples/README follow (03 (cxx); 04 keeps
  the reference backend).
- Decision 6a.  The README of example 04 and the section "Writing
  Compiled Code to Disk" of UsingTheAPI.rst say in one sentence that
  the saved program of the reference backend retires at the removal
  gate of #82 (the API page avoids the phrase "Python form", which
  unit_reference_backend.py forbids in the user guide).
- Decision 6c, the hash rule.  UsingTheAPI.rst, "Built-in Conversions",
  where a node is described: two nodes compare by structure, a node is
  never equal to a value that is not a node, a node hashes by identity,
  so two equal nodes hash differently and a set keyed by nodes keeps
  both, a node is not meant as a dictionary key, a node has no order;
  with a transcript.  No page described the equality against a foreign
  object before; the lane of #82 stage 5 documented it in the source
  and in unit_equality.py alone.  test_hash of unit_equality.py pins
  the rule: node == same, hash(node) != hash(same), len({node, same})
  == 2, same not in {node}, {node: 1}.get(same) is None, both backends.
- Files outside the list of the item that had to change:
  src/python/tools/make.py (the paragraph of the manual on the pairs
  served), tests/lib/testrunner/prepare.py (its module docstring),
  docs/source/Examples.rst and examples/README (the sentences on
  example 03), tests/unit_equality.py (the pin).  Not changed:
  AGENTS.md (the owner's), which states the rule of #101 and says that
  a run of the front end rewrites every .fcy it wrote, and nothing of a
  hit; one clause there could say that a hit of the ICurry cache
  rewrites the .fcy of the module as well.
- Verification, one process at a time unless said otherwise.
  unit_curry2icurry_stale.py: 18 ok on cxx (tiered, 10.8 s), under
  interpret:off (9.7 s) and under interpret:all (9.9 s).  Example 03:
  ./run cold 2.4 s, warm 0.29 s, status 0, the same output; under
  backend:py the last lines differ ("def CyF3Nat8natToInt(rts, _0):").
  Through the runner at -j 2 --mem 4 --backend both:
  unit_curry2icurry_stale, unit_curry2icurry (#99), unit_equality,
  unit_examples, unit_reference_backend, unit_runner, unit_make,
  func_flat2icurry, unit_flat2icurry_bindingopt and unit_inspect: 20 of
  20 run, 19 ok, wall 243 s (unit_examples 168.7 s on py and 27.0 s on
  cxx; func_flat2icurry 39 s per backend).  The one failure,
  unit_inspect.py on cxx, testGetICurry and testGetImpl with Peano
  interpreted ("bytecode: 30 units" for the step of add), as the entry
  of #82 stage 5 saw it in a fresh worktree; after ./run_tests
  --prepare-only --backend cxx unit_inspect.py (the pool, 34.5 s) the
  two tests still failed in a test process, while a probe outside the
  test library loaded Peano from its object (the banner of add).  The
  other item of this lane edits the C++ backend (materialize.py, the
  bindings) in the same worktree, and a later probe failed at the
  import of the backend on a missing binding (set_trap_hook); the file
  is not one of this item.  make -C docs html: status 0, no WARNING
  line (19 s).
- Left to the owner.  (1) The narrowing of the oracle test keys on the
  products of PAKCS beside the .fcy; a front end run by hand
  (pakcs-frontend --flat M) leaves none, and the test fails until
  sprite-make --rewrite-flat M; the report names the command.  (2) The
  entry of the runtime item of this lane was not in the file when this
  entry was appended, so this one comes first.  (3) The clause of
  AGENTS.md above.  (4) A window in a parallel functional run: the
  oracle writes M.fcy before it compiles the module to Prolog and
  writes M.pl, so test_products_on_disk, run beside another functional
  file at -j 2 on a tree where the front end has work for the oracle,
  can see the pair without a product and report it DIFFERENT (not
  reproduced; CI runs the files of a shard one at a time).  The
  docstring of check_on_disk and section 8 of tests/README record it.
  An acceptance by shape instead, an unrewritten .fcy newer than the
  .icy beside it (translated_before_rewrite names it; a route never
  leaves it), would close the window.

2026-10-08: #102 under interpret:off and the step limit of the C++ runtime (decisions 5 and 6b of the action list; worktree wt/potholes3; the entry of the toolchain item of this lane precedes this one)
- #102, reproduced: under interpret:off a module imported from an ICurry
  object (the ICurry of CxxInterp read from its JSON, then
  curry.import_(imodule)) got its functions without a step (has_step
  False), and the first evaluation, fact 3, died with SIGSEGV in
  generate_values; tiered and new gave 6.
- The fix, two lines of defence.  (1) The compile on first use.  Under
  'off' a function table made at run time gets the trap step of the
  runtime (cyrt/module.hpp: trap_step, install_trap, is_trapped,
  set_trap_hook, owner_of; backends/cxx/materialize.py: Materializer.trap
  and one Pending entry per module, with the search path of the import
  from module_loaded).  The first call of such a function calls the hook
  (materialize.first_use), which compiles the module in the process: its
  pending imports first; the tables kept for the process and the shim of
  the module linked and loaded (tiered._register_shim, _load_shim), so
  that the object binds its tables to the tables the nodes and the
  Python objects hold; the C++ of the ICurry object in hand written with
  interp.save beside the source (.curry/<subdir>/M.cpp; a directory of
  the process when the module has no source file or the directory
  cannot be written), compiled by Cpp2So, the step sprite-make --so runs
  (the product cache keeps the object); and the object loaded with the
  swap of tiered execution on the calling thread (cyrt.tiered_adopt;
  Module::adopt takes a trapped table as it takes an interpreted one).
  Then the trap runs the compiled step on the same redex, and cxx.shlib
  names the object.  Why at the first call and not at the import: the
  plan imports every module it compiles the same way (Json2TargetSource,
  which generates code and evaluates nothing), and a compile at the
  import would recurse; the trap fires on a call alone.  (2) The error.
  A function whose table still has the trap when it is called, because
  the hook refused (the test hook materialize.COMPILE_ON_FIRST_USE is
  False), the compile failed, or the module is gone, sets the error of
  its configuration and returns E_ERROR, so the evaluation raises
  EvaluationError "cannot evaluate function 'M.f': the function has no
  compiled code (its module was imported from an ICurry object under
  interpret:off): <reason>" (the module from owner_of, a scan of the
  loaded modules).  The hook runs inside the step, so the trampoline of
  the bindings reports an exception of the hook as the reason; an
  interrupt and an exit pass.  RuntimeState.single_step raises the
  error a step reports (it dropped it), through the Python wrapper that
  turns the exception of the bindings into curry.exceptions.EvaluationError.
  has_step is False for a trapped table, and icurry_attach accepts one.
- The step limit (6b).  RuntimeState::step_limit (NOLIMIT by default,
  cyrt/state/rts.hpp); procS returns the new status E_TERMINATE (-16,
  fwd.hpp) after the completed step that brings steps_total to the
  limit (the test reads the count before the increment: one compare per
  step); procN hands it out with E_UNWIND and the others, hnf and the
  generated code return it as any status below T_CTOR, a nested procD
  yields it to the enclosing evaluation, and the outermost procD throws
  StepLimitReached (exceptions.hpp, bound in the bindings), which
  generate_values turns into control.E_TERMINATE.  So the graph holds
  the result of exactly N steps, no redex is half rewritten, and the
  counters of the rotation do not move (stepcount is not touched;
  unit_cxx_rotation.py pins 16929/1636 and 2112920/511484 as before).
  The interface: the Python wrapper of the state has stepcounter, a view
  (cyrtbindings.StepCounter) with global_count, global_limit and
  reset_global over steps_total, step_limit and _step_base of the state,
  so Evaluator.set_global_step_limit, evaluator.evaluate(..., steplimit=N)
  and cytest.step work unchanged on both backends; a limit at or below
  the count raises at once, as on the Python backend.  The backends give
  the same states (f (g (g 0)): f (g 0) after 2 steps, f 0 after 3; a
  goal of 29 steps: E_TERMINATE at 28 and 29, the value at 30), so
  test_interp_step of unit_py_runtime.py and test_apply_nf of
  unit_prelude.py pass on the C++ backend and their expectedFailureIf
  decorators are gone.  A view and not a field, because a counter held
  by the state made a reference cycle: the state of
  evaluator.single_step (one per compiled expression) then lived until
  the cyclic collector ran, a live state is a root of the C++ collector,
  and the history of a search stayed reachable, so
  test_repl_eval_under_stress of unit_goals.py (last of 8000 elements
  under SPRITE_GC_STRESS=1) grew with the square of the length (1000
  elements 5.0 s, 2000 16.0 s, 4000 46.6 s, 8000 past its bound of 60 s)
  and takes 1.0 s for 4000 again; test_counter_keeps_no_cycle of
  unit_cxx_steplimit.py pins it.
- A finding on the way, fixed: the second half of test_apply_nf
  evaluates id $!! (x::Int) where x free.  The C++ runtime suspended
  (hnf of the free variable at the root of the argument records a
  residual, which applynf_step returned); the Python backend and PAKCS
  3.4.1 give the variable, _a.  applynf_step now takes the residual back,
  as hnf_or_free does, and reports T_FREE, which _applyspecial accepts,
  so ($!!) and normalForm give the variable, and ($##), which needs a
  ground normal form, suspends as before (applygnf_step).
  test_apply_nf_free_variable of unit_prelude.py pins the three on both
  backends.  A change of a primitive beyond the items, made because the
  test the owner asked to flip needs it; apply.cpp, twelve lines.
- The cost of the check, Tak1 through the benchmark harness (run Tak1
  -b cxx -r 1 -w 1 under interpret:off and steps:65536, one run before
  and one after, compare --deterministic): 16203.7 M instructions
  before, 16377.2 M after, +1.07 percent, with 33629915 steps and equal
  forks and collections in both: about five instructions per step.
  Wall 2.20 s before and 1.40 s after, advisory (the machine was busier
  during the first run).  A first form of the check, the compare after
  the increment, measured 16376.5 M, so the compiler shared the load
  already; the rest is likely the code layout of procS (the library has
  three call sites of it; not compared with the baseline).  Not pursued.
- The skips.  unit_optimize_applies.py and unit_optimize_inline.py no
  longer reload under 'new' for the unoptimized half of their value
  tests: ModuleTestCase.interpretable of unit_optimize.py is removed
  (the two files were its only callers), and the module from the ICurry
  object compiles on its first use under 'off'.  tests/README section 4
  still describes the reload (the toolchain item of this lane owns the
  file; the report of this item names the sentences).
- The review of the first use, three findings fixed.  (1) The products
  of a transformed ICurry object (the bodies of two functions swapped in
  memory) went beside the source and into the product cache under the key
  of the texts on disk: a later import of the module by name, in a new
  process and again after rm -rf .curry (the restore), ran the transformed
  code.  Now the C++ goes beside the source, and the object into the
  cache, only when the object in hand is the translation of the texts on
  disk (materialize.is_translation: the JSON beside the source read again
  from the file, taken through the merge and the passes of an import, and
  compared with the object by icurry.json.dumps, which leaves the
  metadata out); any other object, and a module without a source file,
  compiles into the directory of the process (tiered._tmpdir, which
  _productcache.excluded keeps out).  The value tests of the optimizer
  files turn the inliner off through the metadata, so their modules are
  not the translation and compile into the process directory.  (2) A
  current object beside the source was compiled again at every first use
  (the second process rewrote the .so).  Now, for the translation, a
  current object whose generated file has the text of the object in hand
  is adopted without a compile (materialize.current_object: the text, the
  stamp and the objects of the imports through Cpp2So.is_stale), else
  the product cache is asked before the compiler (Cpp2So.restore, as the
  plan does), else Cpp2So compiles.  (3) first_use compiled the module
  again at every call of a table the adopt left trapped (an object
  without a step of that name); it now answers "the compiled object has
  no step for it" at once when the entry is compiled.  Tests in
  TestFirstUse: a transformed object leaves nothing beside the source
  and no cache entry (its source under tests/.cache, outside the excluded
  temporary directory); is_translation on the plan's object after the
  import, with another body, without the JSON, without a file; a current
  object adopted with no call of Cpp2So and the mtime unchanged; the
  cached object restored with no call of Cpp2So; no second compile after
  an adopt patched to swap nothing.  The fourth finding, tests/README
  section 4, is the toolchain item's file (see below).
- Tests.  TestFirstUse of unit_cxx_interp.py (11, C++ backend): the
  compile on first use (trapped before, swapped after, three functions
  in curry.stats()['swapped'], the object with its stamp and the
  generated file beside the source, sofilename, getimpl from the
  generated file, an expression built before the compile evaluates after
  it, no second compile, the error "boom" from the compiled code); the
  error with the compile turned off, through curry.eval and
  evaluator.single_step, and the compile afterwards; the error after a
  failed compile (Cpp2So patched to raise; the warning logged; the
  module stays trapped and compiles later); a pending import compiles
  first; a module without a source file compiles into a directory of
  the process; 'new' and 'tiered' attach bytecode as before; the five
  tests of the review above.
  unit_cxx_steplimit.py (new, 6, both backends): the limit ends the
  evaluation after exactly N steps for every N up to the count of a
  value, the totals count them, and the expression evaluates on from
  there; the stepper on a nullary goal with the inliner off; the
  interface of the counter (inf, None, 0 raises at once, step_limit
  NOLIMIT and 7); no reference cycle (C++ backend); a limit inside a set
  function for every N; a limit inside a search of four values.
  test_apply_nf_free_variable of unit_prelude.py.  The docs: the flag
  interpret in curry.interpreter.flags, EnvironmentVariables.rst, and
  the stepper paragraph of DeveloperNotes.rst.
- Verification (env -i, prlimit, one process at a time; the runner at
  -j 2 --mem 4).  make stage after every C++ change (status 0) and make
  overlay.  Single files on the C++ backend: unit_cxx_steplimit 6 ok
  (on py too, 1 skipped), unit_cxx_interp 31 ok under tiered, off, new
  and all, unit_py_runtime 12 (7 skipped, 1 expected failure) under
  tiered and off, unit_prelude 18 ok under tiered and off (py 1
  skipped), unit_optimize_applies 25 and unit_optimize_inline 31 ok
  under off, tiered and py, unit_cxx_rotation 20, unit_cxx_tiered 20,
  unit_cxx_runtime 5, unit_loadsave 5 (4 expected failures), unit_goals
  48 (4 expected failures).  Through the runner on cxx after
  --prepare-only 'unit_*.py' (the pool 31 s, the benchmarks corpus
  46 s): unit_cxx*.py 21 of 21 ok (121.6 s); the other unit files in two
  halves, 33 of 33 (195.3 s; unit_goals failed there before the view and
  passed alone after it) and 37 of 37 (82.1 s); the whole unit suite
  under interpret:off 91 of 91 ok (329.3 s); the touched files on both
  backends 22 of 22 ok (79.6 s); seven files under SPRITE_GC_STRESS=1,
  7 of 7 ok (150.2 s); func_*.py 21 of 21 run, 20 ok and
  func_goal_defaulting failed inside the sandbox alone (PAKCS writes
  under /tmp) and passed outside it (53 ok).  make -C docs html: status
  0, no WARNING line.  After the review of the first use: a probe of the
  three phases (transformed object, import by name, import by name after
  rm -rf .curry) gave 2, 2, 2 before and 2, 1 (no object beside the
  source, no cache entry) after; the honest import twice rewrote the .so
  before and kept its mtime after; unit_cxx_interp 36 ok under off and
  tiered; unit_optimize_applies 25 and unit_optimize_inline 31 ok under
  off; the runner on both backends over unit_cxx_interp, unit_optimize,
  the two optimizer files, unit_cxx_steplimit, unit_prelude,
  unit_py_runtime, unit_cxx_tiered and unit_product_cache: 18 of 18 ok
  (52.1 s).
- Left to the owner.  (1) id $! x where x free: both backends suspend,
  PAKCS gives the variable.  (2) The cost of the check, about one
  percent of the instructions of Tak1: the layout of procS, or an
  inlining decision, may give it back.  (3) tests/README section 4.

2026-10-08: the conda build of record under the two-line stamp and format 13 (worktree wt/conda2; decision 3 of 2026-10-08)
- The build.  conda/build-packages.sh --skip-frontend --jobs 4
  --memory-limit 16 --timeout 3600 with the conda-build of the build
  environment (conda-build 26.9.1, conda 26.9.1), the PAKCS archive into
  the source cache, CONDA_PKGS_DIRS on the shared package cache, from the
  export of the committed HEAD c8c17fa1 of the worktree (the merge of the
  format-13 lane), no overlay, the curry-frontend of the local channel.
  The first attempt ran inside the sandbox and failed as the README says
  it does: the activation script of the conda-forge binutils wrote
  /tmp/old-env-<pid>.txt on a read-only /tmp (2 min 16 s to the failure,
  after the build environment was made).  The same command with the
  sandbox off: sprite-0.9-py314hc388cfb_0.conda, 2,827,087 bytes, 718
  files, 27 MB on disk (26 MB under opt/sprite), conda-build 5 min 24.0 s
  (the build step 2 min 29.6 s, CPU 3 min 23 s user, 1.8 GB peak; the test
  step 14.1 s; the rest the environments and the packaging), the script
  5 min 41.3 s, another lane on the machine (load about 5 on 12 cores).
  The package replaced the file of the same name in the local channel
  (the build string is unchanged); the third build of 2026-10-07 remains
  as its extraction and its environments.  info/has_prefix: 13 text
  entries, the stamps, and no binary entry (paths.json: 13 paths with a
  placeholder, all text; a grep of the extracted package for the
  placeholder finds the 13 stamps alone).  Against the third build of
  2026-10-07: five files more (backends/cxx/elf.py,
  toolchain/_productcache.py, their bytecode caches,
  sysconfig/cxx_compiler with "15.3.0 x86_64-conda-linux-gnu"), none
  less.  readelf -d of Data/List.so in the package: SONAME
  sprite-Data.List.so.13, NEEDED sprite-Prelude.so.13,
  sprite-Data.Maybe.so.13, libcyrt.so and the four system libraries,
  RPATH $ORIGIN/../../../../../../lib:$ORIGIN/../../../../lib.  The
  warnings of the log are the ones of the earlier builds (the unused
  variable of module.cpp:147, the rpath of the build environment that
  conda-build removes from 15 objects: the 13 library objects, libcyrt.so
  and _cyrtbindings.so).  The test step of the build
  passed (sprite-exec and python -m curry on both backends, sprite-make
  --so, no prefix in a .py or .cpp of the library, the Prelude's source
  and type).
- The environment.  conda create from the local channel and conda-forge
  with python=3.14 (8.4 s, every package cached): python 3.14.8, gxx
  15.3.0 through cxx-compiler 2.0.0, libstdcxx 16.2.0, sysroot_linux-64
  2.39, gmp 6.3.0, curry-frontend 2.0.0 pakcs341_0; the sha256 of the
  conda-meta record is the one of the new file, 718 paths.  The checks,
  each its own command under env -i with the bin of the environment on
  PATH, no SPRITE_HOME, no CXX, XDG_CACHE_HOME in a scratch directory,
  prlimit --as 6 GB and timeout 600 (the script and its log are kept
  beside those of 2026-10-07): (1) the 13 stamps: line 1 the digest of
  the environment 2481ab99fd5c34b5 in all 13, line 2 <env>/opt/sprite in
  all 13, stamp_is_current true for all 13.  (2) readelf -d of the
  installed Data/List.so: as in the package; elf.read_dynamic over the 13
  objects: each with the SONAME of its module, no slash in any NEEDED
  entry.  (3) import curry 0.20 s; the first import of the Prelude with
  _system.pexec wrapped to count the compiler commands: 0.13 s inside the
  process (0.34 s wall), no command, compile 0.0 s, cxx.shlib set, the
  Prelude.so of the environment in /proc/self/maps; a second process the
  same.  (4) sprite-exec of the two-value program that imports Data.List:
  cxx 1.32 s then 0.33 s, py 1.28 s then 0.36 s; python -m curry 1.23 s
  (cxx), 0.32 s (py); interpret:all 4.1 s; sprite-make --icy of a module
  that imports every library module 1.5 s.  (5) the writes under the
  environment after the short runs: lib/python3.14/__pycache__/
  _sysconfigdata__linux_x86_64-linux-gnu.cpython-314.pyc alone (Python's
  own); nothing under opt/sprite.  (6) sprite-make --so of the program in
  a scratch directory 5.67 s (the precompiled header, 71 MB, under
  $XDG_CACHE_HOME/sprite/pch/<key>, and the program), its stamp two lines
  (the digest and the prefix of the environment), its NEEDED entries
  sprite-Prelude.so.13 and sprite-Data.List.so.13, sprite-exec from the
  object 0.33 s; a second module that imports the first: the script ran
  sprite-make --so Second.curry without CURRYPATH, which exited 1 with
  "Curry module 'Smoke' not found"; the measurement with the directory on
  CURRYPATH was taken by hand after the script (CURRYPATH=<dir>
  sprite-make --so Second.curry 0.85 s, CURRYPATH=<dir> sprite-exec
  Second.curry 0.31 s, values 25 and 8); the script line was then
  corrected, and a run of the corrected step was appended to the log
  (0.51 s and 0.19 s, the same values; without CURRYPATH exit 1 with the
  same message); nothing written
  under the environment.  (7) the bytecode caches: 13 of the library and
  183 of the package valid (bytecode_is_current), 196 caches, none with
  an absolute co_filename.  (8) the directories of opt/sprite without
  write permission: a write into the product directory of the library is
  refused; sprite-make --so of the program 1.68 s exit 0 (the header
  present), sprite-exec 0.31 s (cxx) and 0.34 s (py), a program that
  imports Data.Char: sprite-exec 1.14 s, sprite-make --so 0.81 s,
  sprite-exec 0.35 s, the first import of the Prelude 0.14 s from the
  shipped object; no message, nothing written under the environment.
  (9) the integrity after every run: 0 of 718 package files differ from
  the conda-meta record; nothing new under ~/.cache/sprite.  (10) the
  examples 10 and 24 from copies outside the repository with SPRITE_HOME
  of the environment: 1.3 s and 22.5 s, both outputs equal to
  expected.out (24 took 11.9 s on 2026-10-07; the machine was loaded).
  Every check of the task is met; nothing of the recipe, the build script
  or the code had to change.
- One correction of the README.  Open question 13 said the compiled
  program Smoke.so carries ld_interpreter_path in its .interp section.
  It does not: the toolchain saves a module with module_main=False
  (backends/generic/toolchain.py), so _generate_main, which writes the
  section, runs for curry.save with a goal alone; readelf -S finds no
  .interp in the 13 library objects of the environment, in the Smoke.so
  of this test, or in the Smoke.so kept from the tests of 2026-10-07;
  curry.save(M, goal="main") in the environment holds the my_interp line
  with /lib64/ld-linux-x86-64.so.2, curry.save(M, module_main=False)
  does not.  The script of 2026-10-07 ran readelf -l Smoke.so | grep
  interpreter inside an && chain, which ended there, and the next line of
  its log, the PT_INTERP of the Python of the environment, was read as
  the object's.  The README says what the objects show now;
  test_ld_interpreter_path checks the value against the Python, as
  before.  Not changed (the code is not this lane's): whether the section
  should exist at all is the owner's (question 13 keeps the two options).
- Files: conda/README.md (the build of record of this date beside the
  builds of 2026-10-07, the environment test of this date, question 2
  shown by the build, question 3 and decision 6, question 13 corrected,
  "What remains before publication" shortened to the ten items still
  true), this entry.  No change to the recipe, the build script,
  tests/unit_conda.py or the code.  Verified after the edits:
  tests/unit_conda.py through the runner under env -i at -j 1 on both
  backends, 24 OK on cxx (40.6 s) and 24 OK on py (34.5 s).
- What remains, for the owner: the split into sprite and sprite-cxx
  (question 2), the compiler pin (6), the sysroot pin (14), the notices
  of the front end (15), one source of truth for the front-end pin (5),
  the front-end feedstock (1), pip (12), the oracle against the package
  (9), the metadata of the artifact (a build from a neutral path or a CI
  feedstock before an upload), and the channel.  The two stale
  precompiled-header members of 2026-10-07 under ~/.cache/sprite and the
  earlier environments of the lane stay where they are.

2026-10-08: Int is 64 bits with an error at the boundary, and the integer division by zero (#105, #106; worktree wt/apibugs)
- Diagnosis.  The Int of the C++ backend is int64_t, and the boundary was
  silent.  A Python int past the range reached the node through the Arg
  binding: the 64-bit overload of pybind11 refused it, the double overload
  took it, and the node held the bits of the nearest double (2**63 gave
  4890909195324358656).  plusInt, minusInt and timesInt wrapped (2**62 +
  2**62 gave the minimum).  truncate and round of a Float past the range
  cast an out-of-range double (1e30 gave the minimum).  quotInt and remInt
  divided through a double, so quot (2**53 + 1) 1 gave 2**53 and rem
  (2**53 + 1) 3 gave 3.  divInt and modInt divided by zero unchecked, and
  the process died with SIGFPE (exit 136); div minBound (-1) traps the
  same way.  The Python backend kept the Python integer and raised
  ZeroDivisionError.  The owner's default of 2026-10-08: 64 bits with an
  error at the boundary; the unbounded Int is a later item.
- Fix, C++ backend.  currylib/defs/checked.def is the template of a
  primitive on Int whose result may not exist: FUNC returns false, the
  step sets the error of the configuration (as prim_error does) and
  returns E_ERROR: catch takes it, and the scheduler raises it as
  EvaluationError.  math.cpp
  defines the nine checked primitives with the builtins of the compiler
  (__builtin_add_overflow and friends): plusInt, minusInt, timesInt;
  divInt, modInt, quotInt and remInt with the division by zero and the
  quotient of the minimum by -1 (the remainder of that pair is zero and
  is taken before the division, which traps on it); prim_truncateFloat
  and prim_roundFloat, which accept the integral doubles from -2^63 up to
  2^63 and refuse the rest, the infinities and NaN included.  quot and
  rem are integer divisions now, exact above 2**53.  The Arg binding
  (cyrtbindings/graph.cpp) gets an overload for a Python int that the
  64-bit overload refused; it raises CurryTypeError with the text of the
  Python backend.  The literals of the ICurry interpreter
  (cyrtbindings/icurry.cpp, an Int constant and the value set of a case)
  check the range with PyLong_AsLongAndOverflow and raise the same error;
  before, the cast of pybind11 raised a RuntimeError without the value
  and the module did not load.  No node layout changed.
- Fix, Python backend.  currylib/prelude/math.py has the twins: plusInt,
  minusInt, timesInt, the four divisions and the two conversions check the
  64-bit range and the zero divisor and raise EvaluationError with the
  same texts; quotInt is an integer division; prim_divFloat gives the IEEE
  result of a division by zero (an infinity, or NaN for 0.0 / 0.0) as the
  C++ backend does, instead of ZeroDivisionError.  prim_readNatLiteral
  of currylib/prelude/read.py fails the parse of a numeral outside the
  range, as the C++ step does on ERANGE (before this entry it returned
  the wide integer; after the range check of the node it raised the
  CurryTypeError inside the evaluation).  graph/node.py checks
  the range when an Int node is made, so the Python backend never holds a
  wider integer (one mask of the flags per node).  The generated Python
  binds the primitives when the file is written ("from _operator import
  sub as ..."), so compiler.FORMAT_VERSION is 7 and a file of format 6 is
  written again from its JSON; without the bump an installed Prelude.py
  kept the operators of Python (seen: a staged tree ran the old ones).
- The texts.  "integer overflow: 9223372036854775807 + 1", "integer
  overflow: (-9223372036854775808) * (-1)", "integer overflow: div
  (-9223372036854775808) (-1)", "integer overflow: truncate 1.0e+30",
  "integer overflow: round (-Infinity)", "division by zero: div 1 0".  A
  negative operand is in parentheses; a Float is spelled as show spells
  it.  negate x is 0 - x in the Prelude, so negate minBound names "0 -
  (-9223372036854775808)"; ceiling and floor convert through truncate and
  name it.  A Python int outside the range: "9223372036854775808 is
  outside the range of Int (-9223372036854775808 to 9223372036854775807)",
  a CurryTypeError on both backends through curry.expr, curry.raw_expr
  and curry.eval, at the node.
- Cost.  Throughput suite, one run each, clean environment, the objects of
  Tak1 and Fib compiled under it, compare --deterministic: Tak1 16210.3M
  to 16216.8M instructions (ratio 1.00, +0.04 percent), Fib 2348.8M to
  2349.3M (+0.02 percent), steps equal (33629915 and 3500006).  The CPU
  seconds moved with the load of the shared machine (0.90 to 1.37 between
  pairs) and are advisory.  A first pair measured on a cold tree gave
  27880.0M to 16729.8M for Tak1: the first run compiled Tak1 in its
  process tree (the tiered compile under sprite-exec), so the pair was
  discarded and measured again on a warm tree.
- Tests.  tests/unit_int_semantics.py, 13 tests on both backends: the
  boundary values, the Python ints outside the range on three routes and
  at the node, the four operations at the edge, negate and abs of the
  minimum, the signs and the exact quotients of the four divisions, the
  division by zero of each with divMod and quotRem, the Float division by
  zero, the four conversions at the edge and of the infinities and NaN,
  a literal outside the range in a module and in an expression, the
  literals at the ends of the range (-9223372036854775808 negates the
  literal 9223372036854775808 and is refused; -9223372036854775807 - 1 is
  the minimum), read of a numeral at and
  past the maximum, and a compiled program (a literal sum, 1 `div` 0,
  fac 21).
- Documentation.  The Limits section of Using the API states the range,
  the errors with their texts, the literal of a source, the enumeration
  that ends at the maximum, the Float division by zero, read of a numeral
  past the range, and catch.  The section names no backend
  (unit_reference_backend.py greps the user guide for "both backends";
  the first draft failed it on three lines).
- Results.  unit_int_semantics.py 11 ok on cxx and 11 ok on py by hand
  and through the runner.  run_tests -j 2 --mem 4 --backend both:
  unit_prelude, unit_typed_expr, unit_py_evaluation, unit_py_conversions,
  unit_py_graph, unit_py_typechecks, unit_cxx_rewrite, unit_cxx_interp,
  unit_cxx_literals, unit_cxx_runtime, unit_cxx_scheduler, unit_io_bind,
  unit_evaluation, unit_conversions: 26 of 26 (unit_cxx_literals and
  unit_cxx_rewrite after the prepare pass; the fresh worktree had no
  compiled pool); unit_expr, unit_api, unit_goals, unit_typecheck,
  func_math_int, func_math_float, func_math, func_readshow: 16 of 16;
  the other unit_cxx_* and unit_py_* files: 48 of 48, with
  unit_cxx_toolchain and unit_cxx_heap passing under a clean environment.
  A shell that exports the flags of a conda toolchain (CXXFLAGS with -O2)
  reaches the compiles of the runner: the precompiled header is named
  O3O2, and Tak1 keeps Variable::skip out of line.  Run the runner under
  env -i, as the build.  The whole suite is for the suite stage.
- For the owner, not done.  (1) An unbounded Int, the later item.  (2) A
  literal past the range in a Curry source is the CurryTypeError of the
  node on both backends now, when the module loads (C++ backend) or when
  the function is first evaluated (Python backend); the front end accepts
  the literal.  A compile error with the position of the literal would
  name it earlier, and -9223372036854775808 (negate of the literal past
  the range) would need the treatment of the front end of Haskell to be
  the minimum.  One site still differs: a literal past the range in a
  case (isBig 99999999999999999999 = True) refuses the module on the C++
  backend (the value set of the case holds Int nodes) and matches nothing
  on the Python backend (its generated code compares Python ints; isBig
  1 is False).  The check belongs in the emitter of the Python backend
  (backends/py/compiler.py), a file of the other item in this worktree,
  so it is left for the owner.  (3) read of a numeral past the range fails the parse
  on both backends now, the rule the C++ backend had; an EvaluationError
  ("integer overflow: read ...") on both would match the arithmetic
  instead.  (4) catch of the Python backend catches IOError and
  MonadError alone, so the new errors of the primitives pass it there,
  where E_ERROR of the C++ backend reaches catch (caughtDiv = catch
  (return $!! (1 `div` 0)) (\_ -> return 42) is 42 on the C++ backend and
  raises on the Python backend); the gap predates this entry (prim_error
  has it, and unit_io_bind.py skips TestCxxMonadic on the Python backend
  for it).  The parity fix is small (catch EvaluationError, hand the
  handler an IOError with the text), but it changes error under catch on
  the Python backend too.  The Limits section states the difference.
  (5) The texts name truncate for ceiling and floor and 0 - x for negate;
  a text that names the function the user called needs those functions
  as primitives of their own, a change to the Prelude copy.  (6)
  enumFromTo x y computes x + 1 past y, so [9223372036854775806 ..
  9223372036854775807] raises "integer overflow: 9223372036854775807 +
  1" on both backends, where PAKCS gives the two-element list (take 2
  [9223372036854775806 ..] gives it; succ maxBound and 2 ^ 64 raise as
  designed).  A bounded enumFromTo is a change to the Prelude copy; the
  Limits section states the case.
- Suite stage, 2026-10-08.  The whole suite on both backends through the
  runner (-j auto, --prepare, a clean environment, the sandbox off for
  PAKCS): 224 of 224 run, 1 failed, wall 807.5 s.  The failure was
  test_tak_indexes_inline of unit_cxx_heap.py on the C++ backend: a test
  process of the fast tier had compiled Tak1 in the background (the tiered
  compile under the inline budget of the interpreter), and Variable::skip
  stayed out of line; after sprite-make --so Tak1 under a clean environment
  the file passed (10 ok), with unit_cxx_passthrough.py and
  unit_cxx_toolchain.py.  Not of this entry.  The fast tier 136 of 136;
  the oracle suites func_*.py 42 of 42 on both backends; the changed test
  files 22 of 22.  The benchmarks against the committed record
  (phase2-2026-10-04, compare --deterministic): the steps of Tak1 33629915,
  Fib 3500006, Queens10 17706900, Primes 14383977 and Reverse 8407940 are
  the counts the record expects; Tak1 16217.0M and Fib 2349.7M
  instructions in one run with perf (the pair above: 16216.8M and
  2349.3M).

2026-10-08: the unboxed marker outside a primitive, and the walk over shared constants (#107, #108; worktree wt/apibugs)
- #107.  curry.unboxed is the marker of the payload of a primitive,
  [Prelude.Int, curry.unboxed(3)].  Anywhere else the typed builder gave
  the marker the type of its payload and handed the raw Python value to
  make_node as a successor, and the untyped builder returned the raw value
  from its handler: curry.expr(P.id, curry.unboxed(1)),
  curry.raw_expr(P.id, curry.unboxed(1)) and curry.expr([curry.unboxed(1)])
  ended the process on the C++ backend (exit 139; the bindings dereferenced
  the value as a node), the Python backend built the ill-formed node
  <id 1>, and the marker alone returned the Python int.  Both builders
  raise a CurryTypeError now, on both backends.  The untyped builder
  (expressions.ExpressionBuilder) takes the marker under a primitive in its
  symbol handler, make_node(Int, 3), and its handler of the marker raises
  for every other place; the typed builder keeps the Unboxed spec, so a
  description prints it, and TypedProblem._enter raises a ConversionError
  of the catalogue with the position.  The message names the marker and
  its place: "curry.unboxed(1) stands at argument 1 of Prelude.id :: a ->
  a; the marker is the payload of an Int, Char or Float, as in
  [Prelude.Int, curry.unboxed(1)]"; the marker alone "stands alone"; in
  the untyped builder, which has no positions, it "stands outside a
  primitive" (expressions.unboxed_message).  The ValueError "cannot
  rewrite a node to an unboxed value" is gone: the marker alone with a
  target is the same CurryTypeError, and the target stays.  Tests:
  TestUnboxedMarker of unit_expr.py (the payload through both builders;
  alone, as an argument, in a list and in a tuple through expr,
  describe().typeof(), eval, raw_expr and untyped_expr; the message names
  the primitive of the payload; the error carries where and value; a
  target stays).  The docs: Using the API (the boxed and unboxed values,
  the list of the markers) and The curry Package.
  Review findings applied.  A payload of the wrong Python type under a
  primitive built garbage: [Float, unboxed(1)] was <Float 5.0e-324> on
  the C++ backend (the bits of the int as a double) and <Float 1> on the
  Python backend; [Int, unboxed(2.5)], [Int, unboxed('a')], [Char,
  unboxed(1)] and [Int, unboxed(True)] disagreed between the backends the
  same way, through both builders.  Both builders check the payload
  before make_node now (expressions.unboxed_fits, unboxed_payload): a
  Python int under Int (not a bool), a str of length one under Char, a
  float under Float.  The typed builder raises a ConversionError at the
  position of the primitive, "curry.unboxed(2.5) is not the payload of an
  Int at argument 1 of Prelude.id :: a -> a; the payload of an Int is a
  Python int, as in [Prelude.Int, curry.unboxed(3)]"; the untyped builder
  raises the CurryTypeError without the position.  The range of Int is
  checked where the node is made, as before (#105).  The marker has a
  repr, curry.unboxed(1), so the EvaluationError of an item of an
  iterator (converted when the list is demanded) names it instead of the
  default object repr.  Tests: test_payload_type and test_iterator of
  TestUnboxedMarker, both backends.
- Files outside the item that the decision touched, reported here.  The
  runtime tests built Choice nodes as [Choice, unboxed(cid), l, r] and
  heterogeneous tuples through raw_expr, which the decision refuses.
  tests/unit_py_interp.py and tests/unit_py_runtime.py build the choice
  through the choice marker, choice(cid, l, r), the same raw node
  (bootstrap.cid.value is the id; tests/lib/cytest/bootstrap.py keeps the
  marker).  tests/lib/cytest/expression_library.py pairs the variable of
  the value binding with a boxed 2, the shape make_value_bindings of
  rts_bindings.py makes, so test_equals_value_binding of unit_equality.py
  runs on the C++ backend too (its skip said the pair was heterogeneous).
  unit_equality.py maps an unboxed marker to its bare value itself (raw)
  for the rows that compare unboxed values; unit_indexing.py indexes the
  bare values in test_index_unboxed; unit_py_conversions.py asserts the
  error; unit_py_copy.py copies a Choice node, which holds its id unboxed;
  unit_typed_expr.py asserts the error.
- #108.  A value that held the empty list at two types could not be typed
  on the C++ backend: curry.typeof(v) for v from [[1, 2], [3]],
  ['ab', 'c'] or ([1], 'ab') raised MismatchError, and curry.eval(P.unwords,
  strs) with such a value failed the same way; the Python backend typed
  them.  The C++ runtime has one [] node (builtins.hpp: Nil, with Unit,
  True, False, Fail, the small ints and chars, and the partial_node of a
  module); the List spec and string_node of the typed builder make the
  empty list through make_node(Nil), which returns that node; and the walk
  of TypedProblem._walk memoized by node id, so one type variable served
  every position of [].  The walk keeps the memo for the nodes with
  successors and the free variables (a shared subgraph is visited once, a
  cycle ends, a variable has one type) and types a node without node
  successors at every position it holds, with a fresh instance of its
  scheme (builder._constant: a nullary constructor, a partial application
  without arguments, a function without arguments, a failure; not a
  primitive, a string or a generator).  Chosen over the other two shapes:
  a position key for "a nullary constructor shared across types" needs the
  types before the memo decides, and the walk meets the positions one at a
  time; a fresh [] node for every empty list is a change to the runtime,
  the item of the C++ backend, and would not reach the values compiled
  code makes.  The change is sound because such a node carries no type of
  its own: the sharing is an artifact of the runtime, and a fresh instance
  of the scheme at each position is what the front end gave each
  occurrence of the constant in the source.  Every position counts towards
  VALUE_WALK_CAP, so a value of n empty lists counts 2n + 1 nodes on both
  backends, as it did on the Python backend before.  After review the
  walk reads the successors of a node once: a child is pushed with a
  fresh variable, the visit decides from the successors it reads anyway
  whether the node enters the memo, and a shared node pushed twice before
  its first visit unifies with the memo and is not counted again (the
  helper _constant, which read the successors of every child once more,
  is gone).  Measured cost, curry.typeof of a value of the evaluator,
  best of three, the other lane on the machine: C++ backend, a list of
  30000 Ints 1.054 s with the first shape of the walk and 0.888 s after
  (the old memo gave 0.883 s in the review); 30000 empty lists 1.444 s
  and 1.177 s; 15000 (Int, []) pairs 2.118 s and 1.474 s (the old memo
  raised MismatchError on both).  Python backend, 8000 elements: Ints
  0.188 s and 0.175 s (the old memo 0.178 s); empty lists 0.230 s and
  0.220 s (0.129 s); pairs 0.248 s and 0.228 s (0.163 s).  The cost that
  remains on constant-dense values is the typing of each position.  Both
  backends give
  the same answers now: ([], []) is ([a], [b]), (failed, failed) is (a, b),
  (id, id) is (a -> a, b -> b), and a shared Just [] held twice is
  (Maybe [a], Maybe [a]).  Tests: TestSharedConstants of unit_typecheck.py
  (the three values typed and passed to unwords, snd, fst and concat; the
  value of words "ab c"; the constants per position; a shared non-empty
  node, a cycle and a free variable typed once; the cap counts positions
  and a shared cell once).  The docs: Using the API, the values of earlier
  evaluations; the cap is stated in positions there and in the Limits
  (a shared [] counts once per position), since the error text "more
  than 100000 nodes" counts positions on the C++ backend.
- Not fixed, the owner's call (beyond the risk line).  The record of result
  types (builder.TypeRecord) is keyed by the identity of the node, and the
  C++ backend returns the one [] node from curry.expr([]), curry.expr('')
  and curry.expr([], exprtype='[Int]') alike, so the last call wins: after
  e1 = curry.expr([], exprtype='[Int]') and e2 = curry.expr(''),
  curry.typeof(e1) is [Char] on the C++ backend ([Int] on the Python
  backend, where the nodes differ), and the value [] of
  curry.eval([P.filter, P.odd, [2, 4]]) types as [Char] after a
  curry.expr('') in the same interpreter ([a] on the Python backend).
  The conversions of curry.eval are affected too when a value of the
  evaluator is the bare empty list (observed on the C++ backend, the
  Python backend gives the other answers): with v the value of
  curry.eval([P.filter, P.odd, [2, 4]]) and one curry.expr('') after it,
  curry.eval(P.Cons, 1, v) raises MismatchError ("argument 2 has type
  Char") where it gave [[1]] before the curry.expr(''), curry.eval([[1,
  2], v]) raises the same, and curry.eval((v, 0)) yields ('', 0) because
  the walk takes the record of the shared node through its shortcut and
  the converter prints the empty list as a string; after a
  curry.expr([], exprtype='[Bool]'), the value w of curry.eval([P.words,
  '']) fails curry.eval(P.unwords, w).  One curry.expr('') earlier in the
  life of the interpreter is enough: the entry lives as long as the
  shared node.  This is the symptom of #108 for a value that is the empty
  list rather than one that holds it.  The builder alone cannot make the
  record sound: a record skipped for such a node makes curry.eval('')
  yield [] instead of '' on both backends, because the converter reads the
  record.  A mitigation inside the builder, not applied: the walk skips
  the shortcut of the record when the root has no node successors, so a
  bare constant value is typed by its scheme; its visible cost is that
  curry.eval(P.id, curry.expr('')) prints [] on both backends.  The full
  proposals: the evaluator takes the goal's type from the goal object
  instead of the record, and the record skips the nodes the runtime
  shares; or make_node of the C++ bindings returns a fresh [] node to
  Python (the layout stays; the shared Nil of compiled code is untouched).
  The small ints raise the same question in a weaker form: curry.expr(5)
  and curry.expr(5, exprtype='Int') are one node, whose record says
  Num a => a or Int by the last call.  The catalogue (P4) also asks
  whether curry.unboxed should be a public marker at all; nothing in the
  API needs it, since a Python value builds the same node.
- Results.  Baseline before the change, ./run_tests -j 2 --mem 4 --backend
  both over the twelve affected unit files: 24 of 24 run, 1 failed
  (unit_inspect.py on cxx, 2 failures of testGetICurry and testGetImpl,
  which expect a module loaded from its object and found it interpreted;
  present before and after).  After the change: unit_expr.py 30 ok and
  unit_typecheck.py 75 ok on both backends; the fourteen other files of
  the item on both backends 28 of 28 run, every file ok except the
  unit_inspect.py pair above; the fast tier of the unit suite
  (./run_tests -j 2 --mem 4 --backend both 'unit_*.py' --fast) 136 of 136
  run, 2 failed: unit_reference_backend.py on both backends, whose test of
  the user guide fails on the Limits text of the other item ("on both
  backends"), not on this one; func_goal_defaulting.py 53 ok on both
  backends outside the sandbox (PAKCS writes under /tmp, read-only
  inside it).  After the review: unit_expr.py 32 ok and unit_typecheck.py
  75 ok on both backends; the ten other files of the item and
  unit_inspect.py (its corpus object compiled by then) 24 of 24 run, 0
  failed; unit_typecheck_serialize.py, unit_typed_expr.py and unit_api.py
  ok on both backends.  The whole suite on both backends is for the suite
  stage.
- Suite stage, 2026-10-08.  The results of the whole suite, the fast tier,
  the oracle suites and the benchmarks are in the entry above (one
  failure, unit_cxx_heap.py on the C++ backend, from a background compile
  of Tak1 by a test process, not of this entry); unit_expr.py 32,
  unit_typecheck.py 75, unit_typed_expr.py 55 and unit_reference_backend.py
  7 ok on both backends.

2026-10-08: the ABI stamp digests the headers the generated code includes (issue #44, V4; worktree wt/v4)
- The set.  toolchain.runtime_headers returns the include closure of the
  headers the emitter writes into every generated file
  (compiler.RUNTIME_INCLUDES, today "cyrt/cyrt.hpp" alone; vEmitHeader
  writes one directive per name): the scan follows every quoted include
  recursively, resolved as the compiler resolves it (the directory of the
  including header first, then the include directory), a conditional
  include counted (trace.hpp under SPRITE_TRACE_ENABLED and
  state/counters.hpp under SPRITE_SCHEDULER_COUNTERS), the list sorted by
  the path relative to the include directory, so the digest does not
  depend on the order of the directives.  runtime_digest, object_digest
  and the precompiled header (PrecompiledHeader.header_files, which
  decides when a member is stale) take the set.  A header under
  include/cyrt that no include of the closure reaches is out of the
  digest: an edit to it rebuilds the runtime library (the .d files of
  src/Make.depends) and no module.  Kept: an installation without headers
  gives None (the root does not exist), a header that exists and cannot be
  read raises PrerequisiteError (now from the scan, which reads every
  header of the closure), and an include whose target exists nowhere is
  skipped, as the compiler would look on its own search path.  Unchanged:
  the two-line form of the stamp, the key of the product cache, the object
  format (13) and the flags of the flavors; the stamp's silence on the
  compiler flags stays issue #111.  Consequence: the digest changed
  (runtime_digest acf75bd301fe68a1 over 40 headers before, 44945b54ce321163
  over 36 after the mechanical set, 06c21b76385755d3 over 28 after the
  moves below; object_digest of the release flavor 996f8846f99f58c2 before,
  1e3ab57f6792cc66 after; the rewrite of the comment of cyrt.hpp at the
  review moved them once more, to 3ad33630500a2afe and c2c95115e9d64e47,
  the digests of the final diff), so every compiled product is compiled
  once more after this lands: make stage compiled the 13 library modules
  in 37 s (0 restored, 13 stored), and the prepare pass of the test
  corpora (1239 modules in 12 directories on cxx, cold) took 577 s.
- The closure before the moves, measured on the installed tree: 40 headers
  under include/cyrt (32 .hpp and 8 .hxx; graph/gc/block.hpp of src/cyrt is
  not installed, src/cyrt/graph/gc has no Makefile), 36 in the closure of
  cyrt.hpp, 4 outside: graph/equality.hpp, graph/walk.hpp, graph/walk.hxx
  and icurry.hpp.  The orchestrator's scan over the 33 .hpp of src/cyrt
  (29 in; equality, walk, icurry and gc/block out) agrees.  g++ -MM over
  cyrt.hpp under the flags of both flavors lists 33 headers under
  include/cyrt beside cyrt.hpp itself: the 36 of the scan less trace.hpp
  and state/counters.hpp, the two conditional includes; the release list
  and the debug list are the same.  So the mechanical set alone spares
  four headers.
- The moves.  cyrt.hpp included seven headers the generated code does not
  use.  The survey: the generated code of the 13 library modules and the
  1240 generated files of the test corpora of the main tree hold no
  inspect::, no utf8_ name, no Module:: or SharedCurryModule, no throw, no
  name of currylib/prelude.hpp or currylib/setfunctions.hpp (the code names
  the tables of the Prelude by their mangled names, CyI7Prelude4_K_k, and
  declares them itself in its .infotabs.link section; its "Module" is
  bom::Module of bom.hpp); what it uses is Node::create, create_partial,
  partial_node, successor_node, rewrite, forward_to, forward_or_copy, Arg,
  Cursor, Variable, InfoTable, DataType, the bom types, the tags and the
  status values, int_, char_, nil(), unit(), true_(), false_(),
  literal_node, rts->hnf, rts->freshvar and C->cursor.  Six moves, each
  the removal of the directive from cyrt.hpp: dynload.hpp with module.hpp
  (module.hpp includes dynload.hpp, so they go together), inspect.hpp
  (with inspect.hxx), utf8.hpp, exceptions.hpp, currylib/prelude.hpp and
  currylib/setfunctions.hpp.  All six kept, none reverted: after each, the
  generated code of Numeric compiles to an object in both flavors and the
  Prelude's passes the front end of the compiler in both (a check with
  -I src, so no precompiled header served), and with all six make stage,
  make overlay, unit_cxx_toolchain.py (48 OK) and unit_cxx_interp.py (36
  OK) pass, and libcyrt.so exports the same 775 dynamic symbols as before
  (nm -D --defined-only, compared).  The sources that relied on cyrt.hpp
  for those headers got the directives: the nine primitives of the Prelude
  (src/cyrt/currylib/prelude/*.cpp) each include currylib/prelude.hpp
  (the macros of that header name their tables; a source without it would
  compile and export unmangled symbols, so every one includes it, whether
  or not the compiler asked), apply.cpp and constr.cpp include inspect.hpp,
  constr.cpp and io.cpp exceptions.hpp, io.cpp module.hpp and utf8.hpp,
  read.cpp utf8.hpp; of the C++ test programs under tests/data/cxx,
  literals_test.cpp, rewrite_test.cpp and partial_test.cpp include
  currylib/prelude.hpp (failed_Info, apply_Info) and partial_test.cpp
  currylib/setfunctions.hpp (partials_info, PartialS_Info).  cyrt.hpp
  keeps bom.hpp, builtins.hpp, fwd.hpp, graph/indexing.hpp,
  graph/infotable.hpp, graph/node.hpp and state/rts.hpp, with a comment on
  its role.  The counts: headers in the digest 40 before, 36 after the
  mechanical set, 28 after the moves; headers whose edit rebuilds the
  runtime library alone 0, 4 and 12 (currylib/prelude.hpp,
  currylib/setfunctions.hpp, dynload.hpp, exceptions.hpp,
  graph/equality.hpp, graph/walk.hpp, graph/walk.hxx, icurry.hpp,
  inspect.hpp, inspect.hxx, module.hpp, utf8.hpp).
- Tests (unit_cxx_toolchain.py, 48 on cxx, 2 new).
  TestAbiStamp.test_closure: a copy of the include directory; the closure
  of the copy is the closure of the installation, sorted; cyrt.hpp,
  node.hpp, infotable.hpp and rts.hpp are in it and equality.hpp,
  walk.hpp, icurry.hpp, inspect.hpp, module.hpp and utf8.hpp out; an edit
  to every header outside the closure and a new header nothing includes
  leave the list and the digest as they are; an edit to node.hpp,
  infotable.hpp and rts.hpp each gives another digest, and under an
  installation whose include directory is the copy
  (config.installed_path patched for 'include') an object stamped with
  the old digest is stale, accepted_digests lacks the old one, and the
  plan starts again from the .cpp.  test_compiler_lists_no_other_header:
  the generated file includes the roots of the scan and no other runtime
  header, the precompiled header is the first root, and g++ -MM -x c++
  over each root under the flags of each flavor (-MM on the header itself,
  so no precompiled header answers) lists no header under the include
  directory that the scan does not, while every header the scan lists and
  the compiler does not is the target of a conditional include
  (conditional_includes of the test).  test_header_tree_problems adds a
  dangling link inside the closure (the error names it and the
  installation) and an include without a target (skipped, no error).
  test_digest holds as it was.
- The measurements, in the installed worktree under env -i, with the
  product cache of the runs named (SPRITE_PRODUCT_CACHE under the build
  root of the worktree, which the runner does not prune, so the entries of
  the final digest survived the run with the edited node.hpp and served
  the restore).
  (a) A comment line appended to graph/walk.hpp, outside the closure:
  make stage took 8.1 s, compiled 5 of the 41 objects of the runtime again
  (the five translation units that include walk.hpp), relinked libcyrt.so,
  compiled none of the 13 library modules (sprite-make made nothing and
  printed no product cache line) and left both members of the precompiled
  header as they were; run_tests --prepare-only --backend cxx took 12.3 s
  and printed "prepare: 1239 modules in 12 directories on cxx, every
  product present", and 0 of the 1240 objects under tests/data were
  written.  (b) A comment line appended to graph/node.hpp, inside it: make
  stage took 41.8 s, compiled 33 of the 41 objects of the runtime again,
  relinked libcyrt.so, compiled the 13 library modules again (sprite-make:
  product cache: 0 restored, 13 stored) and built the precompiled header
  again (one member, the stale one of the other flavor removed);
  run_tests --prepare-only --backend cxx took 586.2 s, printed the same
  line for 1239 modules, and wrote 1240 of the 1240 objects under
  tests/data: every module of the corpora compiled again, cold (the digest
  was new, so no cache entry served).  Then the two lines removed by an
  edit (git shows both headers as committed), make stage once more (17.3 s;
  the 13 library
  modules restored from the cache of the main tree, 13 restored, 0 stored)
  and the pass once more (27.5 s; 1236 of 1239 from the product cache,
  every product present), so the install and the corpora are those of the
  tree.  The baseline pass that preceded (a), the first compile of the
  corpora under the new digest, took 577 s for the 1239 modules.
- Docs.  Every sentence that said the stamp digests the runtime headers
  says the runtime headers the generated code includes: the docstrings of
  toolchain.py (the module, runtime_headers, runtime_digest,
  object_digest, PrecompiledHeader.header_files, Cpp2So) and of
  _productcache.py, tests/README (sections 1 and 10), AGENTS.md (the
  prepare note and the product cache paragraph), conda/README.md (three
  places), CompilationPipeline.rst, sprite-make.rst,
  EnvironmentVariables.rst (SPRITE_PRODUCT_CACHE) and WithoutCompiler.rst;
  sprite-exec.rst said it already.  The product cache paragraph of
  AGENTS.md still says that an object names its imports by absolute path,
  which format 13 ended; not this item's sentence, left for the owner.
- The deeper split, proposed and not done (the owner's design).  The 28
  headers that remain in the closure divide in two.  The generated code
  needs 14 of them: cyrt.hpp, fwd.hpp, bom.hpp, builtins.hpp,
  graph/infotable.hpp, graph/node.hpp, graph/node.hxx, graph/cursor.hpp,
  graph/cursor.hxx, smallvec.hpp, graph/memory.hpp, graph/indexing.hpp,
  graph/indexing.hxx and state/rts.hpp (for the status values, hnf,
  freshvar and the member cursor of Configuration).  The other 14 come in
  through the complete definitions of RuntimeState and Configuration and
  through inline bodies the generated code never calls:
  state/configuration.hpp (the generated code reads C->cursor and nothing
  else of it), state/queue.hpp, state/scan.hpp and scan.hxx,
  fingerprint.hpp, unionfind.hpp and unionfind.hxx (members of
  Configuration and RuntimeState), state/rts.hxx with ticker.hpp and
  tiered.hpp (check_interrupts, which procS of the scheduler calls, not
  the generated code), trace.hpp and state/counters.hpp (conditional),
  graph/copy.hpp and graph/show.hpp (node.hxx includes them for
  forward_or_copy and Node::str).  A split that gives RuntimeState and
  Configuration a public layout part (the status values, the two entry
  points as declarations on an incomplete RuntimeState, or as free
  functions, and the cursor of a configuration behind an accessor) and
  moves the inline bodies that need copy.hpp, show.hpp, ticker.hpp and
  tiered.hpp into the runtime's own .hxx would spare those 14 headers:
  the digest would cover 14 instead of 28.  The potential in edits,
  measured on the history (git log --no-merges --since=2026-09-01; a
  merge commit is not counted): of the 113 commits, 25 touched src/cyrt
  and 15 touched a runtime header, and each of the 15 compiled every
  module again.  None of the 15 touched only the 12
  headers now outside the closure, so this item spares a kind of edit the
  refresh did not make alone (one confined to the loader, the inspectors,
  the exceptions, the codec or the tables of the primitives); the deeper
  split of the 14 internal headers would have spared 1 of the 15; and
  with the edits of state/rts.hpp itself counted as internal (11 of the
  15 touched it; a public part that holds the status values and the two
  entry points would see few of them) the bound is 7 of 15.  So the
  potential lies in rts.hpp, not in the 14 headers around it.
  The cost:
  a new FORMAT_VERSION if the generated code changes its entry points,
  and the scheduler's inline safepoint moves out of the header the
  generated code sees.

2026-10-08: documentation truth: the statements of P51, the examples of the two API pages, and the facts of P52 (queue item 4 of the absence protocol; worktree wt/docs)
- Scope.  The catalogue of the API study, entries P51 (statements the
  probes contradicted; the examples of Quickstart.rst and
  PythonAPI/UsingTheAPI.rst run as doctests) and P52 (facts the pages do
  not state).  Every row was checked against this tree by a probe on the
  C++ backend in step mode, under prlimit --as 2 GB and timeout 120, from
  a directory with Peano.curry and a probe module Cat.curry (inc, dbl,
  adder :: Int -> (Int -> Int), coin, three, mixed, (+++), a private
  priv).  The text follows the code: no API, flag, default or message
  changed.  The rows of the loader lane (P28, P53, P55) are left as they
  are (#109, #110).
- Rows of P51 corrected, with the place.  (1) "A new interpreter starts
  from the default flags, not from the environment" (Architecture.rst,
  "The Global Interpreter"): under SPRITE_INTERPRETER_FLAGS=
  backend:cxx,stack_limit:1234567, Interpreter().flags['stack_limit'] is
  1234567 and Interpreter().flags == curry.flags; the page now gives the
  order defaults, SPRITE_ROTATION, SPRITE_INTERPRETER_FLAGS, argument.
  (2) "Sprite dedents leading whitespace" (UsingTheAPI.rst, the note of
  "Dynamic Compilation"): str2module writes the text as it is; the note
  says the layout rule accepts the block.  (3) The underscore rule of
  modulename (docstring of compile): compile('x = 1', modulename=
  '_Hidden') puts '_Hidden' in curry.modules; the docstring says the
  module is placed under its name.  (4) getconverter "translates free
  variables into _a" (conversions.py): vals(curry.free()) is
  [<_Free 0 <()>>]; the node passes through, show_value prints _a.
  (5) "the subclasses of MonadError for the errors of IO actions"
  (UsingTheAPI.rst, "Evaluating Expressions"): putChar ('a' ? 'b') raises
  EvaluationError "non-determinism in monadic actions occurred!",
  readFile "nofile" EvaluationError "i/o error: ...", ioError (userError
  "x") EvaluationError "user error: x"; the page says an error of an IO
  action is an EvaluationError whose message names it.  (6)
  "EvaluationSuspended when every alternative suspends" (the same):
  (y + 1 =:= 3) ? True yields True and then raises EvaluationSuspended;
  the page says after the values of the alternatives that did not
  suspend.  (7) "A function applied to more arguments goes through
  apply" (the ArityError entry): expr(inc, 1, 2) is an ArityError,
  expr(adder, 1, 2) is apply (adder 1) 2 (adder :: Int -> (Int -> Int)),
  expr(id, not, True) is apply (id not) True; the entry gives the rule by
  the result type.  (8) "The values curry.eval yields are copies of the
  result" ("Values of earlier evaluations"): after next(curry.eval(e))
  with e = expr(fib, 3), e is <_Fwd <Int 2>>, topython(e) returns the
  node, typeof(e) is Int; with e = expr(plus, 1, 1), e is <Int 2>; the
  page says the goal node is rewritten in place and holds the result or
  forwards to it, with the example of fib 3.  (9) "curry.typeof reports a
  bare type variable" for a compiled goal (the same): Int for '1 + 2',
  Maybe Int for 'Just 5', a for 'xs ++ [3] =:= [1,2,3] where xs free'
  (the lifted root); the page says so.  (10) "The front end checks the
  types.  curry.expr does not." (examples/02-dynamic-code-generation/
  README): expr(inc, 'a') is a ConversionError; the README says
  curry.expr checks them too.  (11) reset "Clears loaded modules (except
  for the Prelude)" (the docstring; CurryPackage.rst, "System Control"):
  after an import of Data.List and a reset, curry.modules holds Data and
  Prelude; both say the packages stay.  (12) "constructors whose names
  are valid Python identifiers are attached" (docstring of CurryModule):
  getattr(M, '+++') is the operator; the docstring says operators attach,
  a keyword and an operator need getattr, a private function is reached
  through curry.symbol.  (13) "by its fully-qualified name or by its name
  relative to a module" (docstring of Interpreter.symbol): symbol('+',
  'Prelude') is ModuleLookupError "Curry module '+' not found"; the
  docstring says the name splits at its first dot and the second
  parameter has no effect.  (14) "Modify this to change the behavior"
  and "Modify this to adjust where to search" (docstrings of
  Interpreter.flags and Interpreter.path; curry.flags and curry.path in
  __init__.py; P54): curry.flags['backend'] = 'py' leaves
  backend.backend_name 'cxx' and the evaluation on the C++ backend;
  curry.path = ['/nonexistent'] leaves getInterpreter().path as it was;
  curry.flags = {...} likewise; interp.flags = {} is AttributeError (no
  setter); interp.path = [...] assigns into the same list; the docstrings
  say in place, the attribute cannot be assigned, backend is read at
  construction; UsingTheAPI.rst "Importing Curry Modules" and
  CurryPackage.rst "Interpreter flags" say the same.  (15) The list
  ambiguity "is not the case, though the reason is subtle"
  (UsingTheAPI.rst, the note of "Symbolic Expressions"): expr([Peano.O])
  is O, expr([Peano.O, Peano.O]) is ArityError, expr([[Peano.O]]) and
  expr([curry.expr(Peano.O)]) and expr(cons(Peano.O, nil)) are [O]; the
  note states the rule by the first element and the three spellings of
  such a list.  (16) "as the REPL of PAKCS prints it" (docstring of
  compile, docstring of absent_from_result, UsingTheAPI.rst "Building
  Expressions"; the REPL page "Free variables in the REPL" gets the
  difference): Sprite prints _a for 'x where x free'; the committed golden
  tests/data/curry/goal_defaulting/text.free.au-gen holds {x=x} x for
  PAKCS 3.4.1.  (17) Found on the way, SPRITE_LOG_FILE "The default, -,
  directs this to standard output" (EnvironmentVariables.rst): the
  handler's stream is sys.stderr; the page says the standard error
  stream.
- Rows of P51 true now, left as they are.  SPRITE_PRODUCT_CACHE: the
  page says the cache is off without the variable; _productcache.root()
  is None and enabled() is False.  curry.typeof on the values of
  curry.eval with a shared [] (P19): '[[Int]]' for the value of
  [[1, 2], [3]], '([Int], [Char])' for ([1], 'ab') (fixed by #108); the
  sentence stays, with "by their content" added.  The usage pages
  (--py, "Three target formats", -b {py,cxx}): the live sprite-make -h,
  sprite-exec -h and sprite-make --man have none of the three; the pages
  are generated from the tools by docs/source/CommandLineInterface/
  build.bash at every build.  The list conversion: the page states the
  loop (a list of 100000 builds; a nesting of 1000 lists is a
  RecursionError, which no row asks the page to state).
- Rows left to the loader lane: ":load FILE compiles and imports the
  Curry module in FILE" (REPL.rst; P53, #110); the effect of a refused
  load (UsingTheAPI.rst, the note of "Writing Compiled Code to Disk";
  P28, #109); the stale object and the load during the compile
  (Quickstart.rst, "Saving Compiled Curry"; P55, #109).  Their sentences
  are as they were.
- The examples of the two pages, run as the catalogue did (Python's
  doctest over the text of the page, the working directory a copy of
  examples/ with Peano.curry, curry in the globals, the C++ backend, step
  mode, option ELLIPSIS).  Before: Using the API 4 of 141 failed (the
  order of curry.modules, the omitted listing of getimpl, Prelude unbound
  at getattr(Prelude, '++'), Fib.so missing), the Quickstart 4 of 41
  (dir, help, the loop without a body, goal_with_two_values) and the
  process dumped core at exit (#109).  Fixed: curry.modules is shown
  through sorted (the order of the dict differs between sessions: Peano
  first in the doctest, Prelude first in a probe); print(Peano.add.
  getimpl()) shows the banner /****** Peano.add ******/ with an ELLIPSIS
  directive, since the listing is the bytecode or the C++ by the moment
  of the call; from curry.lib import Prelude precedes its first use; the
  load of Fib.so has the shell step sprite-make --so Fib.curry in a code
  block before it, the words "in a new Python process", and a SKIP
  directive; dir(curry) and help(curry) carry SKIP; the loop prints the
  value and breaks (S (S O)); the tip defines two = 'S O ? S (S O)' and
  prints S O S (S O).  Sphinx trims the directives from the rendered
  pages (0 occurrences of "doctest: +" in the HTML).  After: Using the
  API 0 of 151 examples failed (1 skipped), the Quickstart before "Saving
  Compiled Curry" 0 of 41 on both backends; the whole Quickstart fails
  in its last section in one of two ways (FileNotFoundError on Peano.so
  while the compile ran; or the load passes and the process dumps core
  at exit).
- tests/func_docs.py: each page in a child process of the Python of the
  installation (prlimit 2 GB, timeout 300 s, CURRYPATH and PYTHONPATH
  dropped as unit_examples.py does), from a fresh directory with
  examples/Peano.curry, on the backend of the session, option ELLIPSIS;
  every example must pass.  Using the API on the C++ backend alone (it
  names Fib.so); the Quickstart up to "Saving Compiled Curry" on both
  backends; the whole Quickstart an expectedFailure on the C++ backend
  that names #109.  A functional file because every run compiles Curry
  with the front end; no oracle, no corpus under data/curry (so --changed
  selects it by name only: docs/* and *.rst are IGNORED in
  selection.py).  tests/README section 6 describes it; the manifest has
  its entries from --update-manifest on this machine (12 cores, 23 GB,
  the calibration machine): cxx 5.5 s, 215 MB; py 0.5 s, 54 MB.
- Facts of P52 added, with the place.  The data model of a node (bool,
  len, [], iteration; P15, P16): UsingTheAPI.rst "Built-in Conversions",
  beside the hash rule, with an example (every node is true, bool of the
  node False included; len undefined; v[0] the head, v[1] the tail,
  v[2] and list(v) TypeError).  The repr of a string (P23): the example
  curry.expr('ab') there.  The forward node (P17): "Values of earlier
  evaluations", with an example.  curry.typed is unchecked (P22): after
  the curry.typed example (typeof(typed(node, 'Bool')) is 'Bool').  A
  str as a goal is a value (P12): "Evaluating Expressions"
  (curry.eval('1 + 2') yields the string).  An unknown converter name
  means no conversion (P13): the docstring of eval and of getconverter
  (converter='bogus' gives [<Int 2>]).  The generator contract (P27):
  "Multi-Valued Computations" (1 ? error "boom" ? 2 yields 1 and raises;
  a kept generator continues after other evaluations: 1, then [4, 5]
  elsewhere, then [2, 3]).  The exhausted iterator and range (P9): the
  iterator bullet and Limits (length of a used iterator 0;
  expr(range(3)) CurryTypeError "cannot build a Curry expression from
  type 'range'").  The tuple limit (P8): the tuple bullet and Limits
  (tuple(range(16)) types and fails with SymbolLookupError "module
  'Prelude' has no symbol '(,,,,,,,,,,,,,,,)'").  The Int range (P3): a
  pointer from the int bullet to Limits, which states it.  import
  curry configures the root logger (P37): EnvironmentVariables.rst,
  SPRITE_LOG_FILE (basicConfig: a StreamHandler on stderr or a
  FileHandler, format '%(asctime)s [%(levelname)s] %(message)s', the
  level of SPRITE_LOG_LEVEL; a host that configured logging first keeps
  its format and level, and SPRITE_LOG_LEVEL=DEBUG changes nothing then).
  import_ (tools 2.5): "Importing Curry Modules" (a dotted name imports
  the packages, import_('Data') is the package, is_sourcefile=True for a
  file, a list of names; the products under .curry/sprite-pakcs-3.4.1/:
  .icy, .fint, .icurry, .json.z, .cpp, .so, .so.abi).  The auto-import
  (P33): CurryPackage.rst "Symbols & Types" (22 library modules in
  automodules; module('Data.Char') imports it, module('Nope') is
  ModuleLookupError).  The three rules of module attributes (P34):
  "Inspecting Symbols"; the keywords not, and, or, return: the Quickstart
  and Using the API at the getattr sentence (dir(Prelude) holds the seven
  keywords False, True, and, break, not, or, return).  The whitespace
  rule of the prompt (P47): REPL.rst "Commands of the REPL" (the prompt
  prints a b for putStrLn "a   b"; :eval 'putStrLn "a   b"' on the
  command line prints a   b).  exprtype and a comment keep the clause
  local (P26): "Building Expressions" and REPL.rst (the REPL prints True
  for x =:= 1 where x free -- comment and {x=1} True without the
  comment).  curry.path and curry.flags in place (P54): "Importing Curry
  Modules" and the docstrings.
- Facts of P52 that need nothing.  The division by zero (P40) and the
  Int range (P3) are in Limits.  truncate and round of a Float past
  2**63 raise now (EvaluationError "integer overflow: truncate 1.0e+30"
  for the Python float, "truncate 9.999999999999999e+29" for the Curry
  literal 1.0e30, whose value the front end computes), as Limits says;
  nothing added.  The shared [] (P19) is stated since #108.  The effect
  of a refused load (P28), the edited source (P53) and the load during
  the compile (P55): left to #109 and #110.
- Verification.  The probes: the scripts of the run under the caps on the
  C++ backend, outputs as quoted above.  Doctests after the edits:
  UsingTheAPI.rst 0 of 151 failed in 4.5 s warm; Quickstart.rst 0 of 41
  before its last section on cxx and on py.  tests/run_tests -j 2 --mem 4
  --backend both func_docs.py: py 3 tests ok (2 skipped), cxx 3 tests ok
  (1 expected failure), 6.5 s.  tests/run_tests -j 2 --mem 4 --backend
  both unit_reference_backend.py unit_runner.py: 4 of 4 ok (the added
  lines of the pages name no reference backend; the manifest reads).
  make -C docs html in a clean environment: finished, 0 warnings; the
  section references PythonAPI/UsingTheAPI:Inspecting Types and :Limits
  resolve; the generated usage pages hold none of the stale phrases.

  Review of the entry, applied the same day.  The keyword list of the two
  pages names the seven Prelude names that are Python keywords (not, and,
  or, break, return, True, False).  The iterator bullet and Limits state
  the rule by demand: of two expressions built on one iterator, the one
  evaluated first takes the items, and the other gives [] (probe: b built
  second, evaluated first, gives [1, 2]; a then gives []).  The index
  rule names the built-in values: curry.expr(1)[0] is 1, not a TypeError.
  SPRITE_LOG_FILE: an unknown level name stops the import with an error
  also when a host configured logging first (OSError from config.py).
  The forward node: <_Fwd <Int 2>> is the form of the example;
  <_FwdSz <O> 24> is the form for a constructor value (Peano.add O O).
  The long added sentences of the pages, the docstrings, the example
  README and tests/README were split, one fact each.  After the review:
  func_docs.py and unit_reference_backend.py 4 of 4 ok on both backends;
  make -C docs html 0 warnings.

2026-10-08: the refused load, the load during the background compile, and the edited source (#109, #110; worktree wt/apibugs2)
- #109, path A, the refused load.  Reproduced (the case D of the API
  study: curry.load(soA), the refused curry.load(soB) of a second object of
  module M, then curry.eval(M.main)): exit 139.  gdb: the fault is in
  cyrt::pack under Node::create, which reads info->format of a table of M.
  The cause, confirmed by the trace: the loader opened the second library
  before it compared the registered file.  The dynamic linker binds the
  symbols of a library to the first definition in the global scope, and a
  compiled module is linked with -Bsymbolic-functions, which binds its
  functions and not its data, so the second library's references to its
  own tables resolved to the tables of the first library; InfoTable has a
  constructor, so the tables are dynamic initializers (.init_array), and
  the load of the second library constructed its tables at the addresses
  of the first library's tables, with name, format and step pointers into
  the second library.  The refusal dropped the second library (the
  loader's SharedCurryModule was garbage), dlclose unmapped it (a compiled
  module defines no STB_GNU_UNIQUE symbol, so nothing marks it NODELETE),
  and the tables of the loaded module pointed into unmapped memory.  The
  cases I and J of the study passed here before the change; they are the
  same mechanism with a reset between (a reset does not rewrite the tables
  of a module loaded from its object), and the fix covers them.  Fix: the
  decision is taken before any dlopen.  The module name comes from the
  SONAME of the object (elf.read_dynamic, which the loader read already
  for the NEEDED entries; toolchain.module_of_soname), and the registry of
  the runtime is asked by that name through a new binding,
  SharedCurryModule.find (the registry is the truth: a library stays
  registered after a reset while its tables are kept, where
  interp.modules and the Module of the runtime know nothing of it).  A
  different file is refused with the documented message, and nothing was
  opened.  The compare after the open stays as the backstop for an object
  without the SONAME of its module (one written before format 13), whose
  name is known only after the open; such an object is still opened before
  the compare, as before.  No other route was smaller: the bill of
  materials of the file needs the relocations applied to read its name,
  and an open with RTLD_LOCAL binds the references of the object to the
  global scope all the same.
- #109, path B, the load during the background compile.  Reproduced (the
  probe of P55: Peano.curry newer than Peano.so, from curry.lib import
  Peano, curry.load of the object, then the exit; also with curry.save
  before the load): "exiting normally" printed, exit 139.  The trace.
  gdb: __strlen_evex under cyrt::Module::clear() under Module::~Module,
  called from the pybind11 deallocator during the garbage collection of
  the exit of Python (Py_RunMain, _Py_Dealloc).  Module::clear keeps the
  tables of an interpreted module and reads info->name to key them; the
  name pointed into unmapped memory.  LD_DEBUG=files: the loader opened
  Peano.so ("dynamically loaded by libcyrt.so"), the shim of Peano was
  "needed by Peano.so (relocation dependency)", so the object bound its
  tables to the live tables of the interpreted module, and "calling fini"
  and "destroying link map" of Peano.so followed at once: import_ returned
  the module of the import, nothing kept the loader's SharedCurryModule,
  dlclose unmapped the object, and the live tables kept the name, format
  and step pointers the initializers of the object had written.  So the
  place is the drop of the library inside curry.load, not the child
  replacing the mapped file (Cpp2So unlinks the old object and the linker
  writes a new inode, so a mapped object stays), and not the order of the
  exit handlers (the exit only reads what dangled already).  A use of the
  module right after the load crashes at once in Node::create (the
  variant with an evaluation), so the hazard does not need the exit; and
  tiered.wait before the load exits 0 (the swap came first, and the load
  is then the benign same-file case: the registry names the file, the
  second dlopen returns the mapped object, no initializer runs).  Nor does
  the hazard need a running compile: under tiered any curry.load of the
  object of a module that runs interpreted binds through the shim the
  same way, whether its compile failed, was blocked, or never ran for the
  lack of a compiler.  Decision: the wait.  When a compile of the module
  is queued, running or finished and not applied (tiered.pending, a record
  the Python side keeps from submit to poll or cancel), the loader waits
  for that module (tiered.wait_for: the compiles queued before it end
  first, the worker is FIFO) and logs one line at the INFO level; the swap
  then loads the object the compile wrote, the module is loaded from its
  object, and the load proceeds as the same-file case.  The wait is sound
  because after the swap the object is kept by the Module of the runtime
  and the registry names it; a refusal would have made the sequence of the
  Quickstart (save, sprite-make --so, load) an error whenever the import
  had started a compile.  The sibling case the trace exposed, a module that
  stays interpreted with no compile pending (its compile failed, or no
  compiler), is refused before the open: "the module runs, or ran,
  interpreted in this process; load the object in a new process" (the
  wording and the scope after the review, below).  A load
  of such an object would need the swap itself (the shims, the link to the
  Module, the check that the object is the translation of the ICurry in
  memory: the route of the trap step in materialize.compile_pending), which
  is a design of the loader and the owner's.  curry.load of an object of a
  module that runs interpreted under interpret:new keeps its old meaning
  (no shim, the object's own storage, the loaded module comes back): the
  round trip of unit_loadsave.py relies on it.  The wait has no timeout; a
  Ctrl-C reaches it between the slices of a quarter second.
- #110, cause (a), the edited source.  import_ of a loaded name returns
  the loaded module and now logs a warning that names the module and the
  file and says that a new process reads the edited file; the REPL's :load
  logs it at every :load (commands.load calls warn_edited with always=True
  before import_, which then says nothing more for that edit).  The notice
  is the module logger at the WARNING level, the convention of the front
  end's warning on overlapping rules and of the notices of tiered
  execution; not the warnings module.  import_ logs it once per edit of a
  module (a memo keyed by the name, the file and its modification time),
  because every import of a module that imports M calls import_(M), and
  the expression module of every :eval imports the loaded module: without
  the memo one edit would print a line at every evaluation.  The check:
  the import records the modification time of the source (st_mtime_ns;
  Handle.source_mtime, set by record_source inside ImportEx), and the
  check compares it with the file.  Not the products' times, as the item
  suggested: the child of the background compile makes the products again
  from the edited source (its front end runs when the source is newer than
  the .icy), so after it the products are newer than the source while the
  module in memory is the one of the first text (seen in the probe of
  cause (b)); the product times then say "current" exactly where the
  warning is due.  A module without a source file, or whose file is gone
  (a module compiled from a string), records nothing and is never stale.
  The message says a new process, not the REPL's restart: :set backend
  reloads the interpreter and loads the module again while repl.module
  still holds the old module object, and on the C++ backend a module of
  the same name takes its tables back with their steps while such an
  object is alive (P30), so the REPL has no restart short of a new
  process; the REPL page says quit and start again.
- #110, cause (b), the swap.  Chosen: the check at poll.  A job carries
  the digest of the ICurry file (.icy) of the module at the import (size
  and CRC-32, tiered_file_digest: one implementation in the runtime, which
  the Python side calls at submit; the JSON at first, changed after the
  review, below), and apply (cyrt/tiered.cpp) refuses the object when the
  digest of the file differs at the swap, after the shims are loaded and
  before the dlopen; the result carries the text TIERED_EDITED, poll logs
  a warning, and the module stays interpreted with the code the import
  read.  The child cannot be told to compile from the products in
  hand: sprite-make takes a module or a source, and the plan starts from
  the newest file of the chain, which an edit makes the source; a new
  option of sprite-make is the owner's.  A refused swap counts as a failed
  module in tiered.status and in failed_compiles of curry.stats.  The two
  fields of TieredJob changed tiered.hpp, a runtime header under
  include/cyrt, so the ABI stamp changed and every object compiled before
  is stale: make stage compiled the library again (48 s), and the prepare
  pass compiled the pool of the tests (101 modules in two directories,
  72 s).
- Tests.  unit_loadsave.py, TestRefusedLoad (C++ backend): the cases D, I
  and J of the study, each in a child process that prints a marker and
  ends with status 0, with the registry naming the first file after the
  refusal; the two objects come from sprite-make once for the class.
  unit_cxx_tiered.py, TestLoadDuringCompile: the load that waits for the
  compile (the INFO line, the swap counted, the module loaded from its
  object, a value); the load during the compile in a child that exits 0
  with a marker; the Quickstart sequence twice with an edit between, each
  run a child; and the refusal of the object of a module that stays
  interpreted, which runs on.  unit_reload.py (both backends): the warning
  of import_ once per edit, silent for an unchanged source and for a
  module without a source file, the values unchanged; :load through
  commands.load once per :load; and, on the C++ backend, the swap refused
  after an edit (the dependent's compile waits behind its import's, so the
  edit lands before the child runs), the module interpreted with the
  values of the import, the warning, and the import after the edit.
- Docs.  REPL.rst under :load; Using the API, "Importing Curry Modules"
  and the note of "Writing Compiled Code to Disk"; the Quickstart, "Saving
  Compiled Curry", names the background compile where its sequence meets
  it; Status.rst, the gaps of the mixed mode; tests/README, section 4.
  make -C docs html builds without a warning on those pages.
- Assessed, left to the owner (beyond the risk line).  A true reload of a
  module in one process: the tables of an interpreted module live for the
  process and a module of the same name takes them back with their steps
  while a module object is alive (P30), the registry keeps one library per
  name, the bytecode of other modules and compiled objects name the tables
  by address, and live expressions hold nodes of those tables; a reload
  that gives the dependents, the expressions and the registry a sound
  meaning is a design of the runtime.  A :reload command of the REPL (as
  PAKCS has) needs that reload, or a re-exec of the process.  A stamp or a
  record that names the digest of the JSON the object was compiled from
  would let the loader verify an object against the ICurry in memory and
  would make the adopt route of curry.load possible for an interpreted
  module; that is a change to the object format or the stamp.  The
  backstop for an object without a module SONAME opens before the compare,
  as before.  The refusal of the object of a module that stays interpreted
  is a new refusal in the shape the issue decided, named here for the
  owner's eye.
- Results.  The probes before the change: P55 exit 139 (also with a use
  after the load, and with the save before it); tiered.wait before the load
  exit 0; case D exit 139, cases I, J, K exit 0.  After the change: every
  P55 variant exit 0 with the INFO line and 7 functions swapped; the cases
  D, I, J, K exit 0 with ['42'] after the refusal.  ./run_tests -j 2 --mem
  4 --backend both: unit_reload.py, unit_loadsave.py, unit_importer.py 6
  of 6 ok; unit_cxx_tiered.py (24 tests on cxx), unit_goals.py (48) 4 of 4
  ok; after --prepare-only on cxx (101 modules, every product present):
  'unit_cxx_*.py' on cxx 21 of 21 ok (97 s); unit_api.py, unit_expr.py,
  unit_loadsave.py, unit_py_conversions.py, unit_py_evaluation.py,
  unit_inspect.py, unit_reference_backend.py on both backends 14 of 14 ok;
  the fast tier ('unit_*.py' --fast) on both backends 140 of 140 ok
  (203 s); the fourteen unit files outside the fast tier that no run above
  covered (unit_examples.py, unit_conda.py, unit_currylib.py,
  unit_benchmarks.py, unit_split.py, unit_equality.py, unit_cache.py,
  unit_configure.py, unit_runner.py, unit_curry2icurry.py,
  unit_product_cache.py, unit_flat2icurry.py, unit_py_fairness.py,
  unit_prebuild.py) on both backends 28 of 28 ok (425 s).  The oracle
  suites (func_*.py) and the whole suite on both backends are for the
  suite stage.
- The review of the change (the same day), the findings applied.
  (1) The digest of the swap was of the .json.z the import read.  The
  child's sprite-make --so writes the JSON in the plain form and the
  import's plan in the compact one, so a touch of the source, or an edit
  of a comment, made the child run the front end again and refused the
  swap (TIERED_EDITED) for a program that did not change, and the module
  lost its compiled tier for the session.  The digest is of the .icy now,
  which the front end writes byte for byte the same for the same program
  (the port and icurry agree, tests/README); the fields of TieredJob are
  named icurryfile and icurry_digest (a header change, so the ABI stamp
  changed once more and the library and the pool were compiled again).
  Test: unit_reload.py, test_edit_of_a_comment_keeps_the_swap.
  (2) The wait before a load skipped when the registry named the kept
  library of an earlier incarnation (after a reset the same path is
  registered while the second incarnation runs interpreted with a compile
  pending): the loader waits whenever a compile of the name is pending,
  before it asks the registry.  Test: unit_cxx_tiered.py,
  test_load_waits_after_a_reset.
  (3) A :load, or an import_ with is_sourcefile, of another file of the
  same module name returned the loaded module and said nothing: a warning
  names both files now (import_.warn_other_file; commands.load passes the
  file the argument names, or the .curry beside a bare module name).
  Test: unit_reload.py, test_load_of_another_file_warns.
  (4) The child of test_quickstart_sequence_twice ran the foreground
  sprite-make --so while the background compile of the import wrote the
  same M.cpp and M.so: a race (a torn M.cpp fails one compile, and a failed
  background compile turns the load into a refusal).  The child waits for
  the background compile before sprite-make, as a session at the keyboard
  does; the load during the compile is its own test.  The Quickstart says
  to run sprite-make once the background compile ended.  A lock on the
  products directory, so that two compiles of one module never write at
  once, is the owner's.
  (5) The dead case 'K' of TestRefusedLoad.run_case is a test now: the
  same file loaded twice, then a use.
  (6) loader.module_name had no caller: removed (_dynamic_names stays).
  (7) The refusal of the object of a module that runs interpreted was
  gated on the name in interp.modules.  After a reset the name is gone
  while the tables of the interpreted module are kept for the process and
  its shim names them; a load then accepted bound the object to the kept
  tables (confirmed here: the probe of the review, M imported stale, the
  compile cancelled, a reset, curry.load accepted, then curry.eval of
  M.main dumped core at once, status 139; the review saw the crash one
  step later, at the import after a second reset, once the new module had
  released the object).  The loader refuses whenever a shim names the
  tables of the name and no library is registered under it, in or out of
  interp.modules: "the module runs, or ran, interpreted in this process;
  load the object in a new process".  The case "swapped, reset, load of
  the same file" is unchanged: the registry answers first.  The plan's
  import of a current object after such a reset meets the same refusal,
  as a DynloadError of the import; the alternative was the crash at the
  next reset.  Test: unit_cxx_tiered.py, test_reset_then_load_is_refused
  (a child process, status 0, the module imports and runs after the
  refusal).
  (8) The wait of curry.load was announced at the INFO level alone, under
  a default level of WARNING: after five seconds (loader.WAIT_NOTICE_SECONDS)
  one line at the WARNING level names the module and the object.  A bound
  on the wait, after which the load refuses, is a new default and the
  owner's; the wait runs in slices of a quarter second, so Ctrl-C reaches
  it.
  (9) SharedCurryModule.find handed Python a reference into an entry the
  registry owns weakly: the binding is find_sofilename now and returns a
  copy of the file name, or None.
  (10) The warning of import_ is once per edit of a module object, not
  once per call of import_ from user code: the review asked for once per
  import_.  Every import of a dependent, the expression module of every
  evaluation and the materialization of the Python backend call
  interp.import_ of a loaded name, and nothing tells a user's call from
  those without a keyword threaded through six internal callers (or a new
  keyword of the public import_), which is beyond the smallest change.
  The memo moved from a process-wide dictionary to the module object
  (Handle.warned_mtime), so a second interpreter that loaded the module
  warns on its own; the once-per-call rule is the owner's to confirm.
  The REPL prints it once per :load as decided.
- Suite stage (2026-10-08, after the review's fixes; nothing committed).
  make stage and make overlay were current (exit 0, 16 s).  Every run
  through ./run_tests -j 2 --mem 4 under env -i: --prepare-only on both
  backends 24 of 24 ok (477 s, the pool compiled again under the new
  stamp); --changed on both backends 228 of 228 run, 2 failed:
  func_goal_defaulting.py on each backend, because the oracle PAKCS
  writes under /tmp and the sandbox of the session mounts it read-only
  (the environment, not the diff); the same file passed on both backends
  outside the sandbox.  unit_reload.py, unit_loadsave.py,
  unit_cxx_tiered.py, unit_importer.py on both backends 8 of 8 ok (54 s);
  --fast on both backends 140 of 140 ok (234 s); unit_loadsave.py,
  unit_cxx_tiered.py, unit_importer.py, unit_cxx_interp.py,
  unit_inspect.py under backend:cxx,interpret:off 5 of 5 ok (46 s); the
  oracle suites 'func_*.py' on both backends 42 of 42 ok (77 s); the
  whole suite with --prepare on both backends 228 of 228 ok (634 s).
  make -C docs html: 0 warnings.  run_benchmarks -r 1 -b cxx --no-perf on
  Tak1 and Fib against phase2-2026-10-04: counters equal (Tak1 33629915
  steps, Fib 3500006 steps), verdict same.

2026-10-08: the READMEs a user reads name no Python backend (queue item 5 of the absence protocol; worktree wt/docs)
- Scope.  Stage 5 of #82 (the entry "the reference flag of the Python
  backend" above) took the Python backend out of the user guide and pinned
  that with unit_reference_backend.py over docs/source.  The README of the
  repository and the READMEs of the examples kept sentences on both
  backends.  A scan with the patterns of the test over README,
  examples/README and examples/*/README found 21 lines in 17 files.  The
  rules: a sentence that only compared the two backends goes; a timing
  sentence keeps the number of the C++ backend; a sentence on a fix keeps
  its meaning; the sentence of example 04 on the saved program follows
  PythonAPI/UsingTheAPI.rst ("The saved program of the reference backend,
  a script in Python, retires at the removal gate of issue #82").  Each
  paragraph touched is wrapped again at the width of its file.  Nothing an
  example prints or does changed; every expected.out is as it was.
- The hits of the patterns, before and after, by file.  README (line 46):
  "runs configure and make stage in a clean environment, and runs a smoke
  test on both backends" is "..., and runs a smoke test" (the usage text
  of scripts/setup-dev-machine.sh says the same).  00 (58): the paragraph
  "The output is identical on both backends." is removed; (64) "A warm run
  of ./run took about 1 s on the Python backend and 0.4 s on the C++
  backend." is "A warm run of ./run took 0.4 s on the C++ backend."  01
  (60), 02 (69), 15 (232), 16 (97), 24 (393): the paragraph "The output is
  identical on both backends." is removed.  04 (36): "The Python form of
  the saved program retires at the removal gate of issue #82 (the
  developer notes), and this example with it." is "The saved program of
  the reference backend, a script in Python, retires at the removal gate
  of issue #82 (the developer notes).  This example retires with it."  10
  (75): the sentence "The output is identical on both backends." is
  removed before "A size with no solution ...".  11 (98): "The Python
  backend did not finish the two grids in 120 seconds, so the test suite
  runs this example on the C++ backend only." is removed; "The first run
  adds about a second to compile the module." stays.  13 (144), 14 (92),
  17 (110), 20 (301), 23 (152): the sentence "The output is identical on
  both backends." at the end of a paragraph is removed.  13 (150): "A warm
  run of ./run took 0.2 s on the C++ backend and 1.0 s on the Python
  backend." is "A warm run of ./run took 0.2 s on the C++ backend."  18
  (102): "The output is the same on both backends." is removed.  19 (140):
  "The output is identical on both backends." is removed.  20 (96): "The
  bindings are sorted, so that the output is the same on both backends."
  is "The bindings are sorted, because the scheduler does not promise an
  order of the values."  24 (410): "The pattern rule binds the free inputs
  of the relation on both backends (#98)." is "The pattern rule binds the
  free inputs of the relation (#98)."
- Six sentences that name the backends in the plural, outside the
  patterns, found by a grep for "backend" over the same files and changed
  by the same rules.  02 (77): "so ./run took 0.8 s on each backend" is
  "so ./run took 0.8 s".  10 (41) and 23 (46): "The scheduler does not
  promise an order of the values across the backends, so ..." is "The
  scheduler does not promise an order of the values, so ...".  15 (65):
  "The order of the values is not promised, and it differs between the
  backends." is "The order of the values is not promised."  16 (74) and
  19 (142): "the first one found may differ between the backends" is "the
  first one found may differ from one run to the next" (both drivers take
  the first value with next(); UsingTheAPI.rst, "Multi-Valued
  Computations": on the C++ backend in time mode, the default, the order
  may differ from one run to the next).
- Left as they are.  The sentences that name the C++ backend alone (the
  timings of 11, 12, 13 and 22; "For example, on the C++ backend:" before a
  SPRITE_INTERPRETER_FLAGS=backend:cxx command in 10, 14, 17, 19 and 22):
  the patterns allow them.  The run scripts and comments that name
  backend:py (examples 03 and 04, examples/03-inspect/go.py,
  examples/cxx/CxxType.curry), as stage 5 left them.
- tests/unit_reference_backend.py: USER_READMES names the files (README,
  README.md, examples/README, examples/*/README); TestUserGuide.readmes
  lists them and test_readmes_name_no_python_backend scans them with the
  same PATTERNS and the same report format, and asserts more than 20 files;
  the docstring of the module names the READMEs.  The existing tests are as
  they were.  Before the edits the new test listed the 21 lines above.  The
  test does not guard the words "each backend", "between the backends" or a
  list marker such as "(py, cxx)": a pattern for them would change the
  existing test over docs/source.
- Review of the same day, applied.  24 (406): "A set function applied to
  such a name exhausts memory on the C++ backend (#97)" was the present
  tense of a fixed bug (#97 is closed; the entry of 2026-10-07 above says
  the module answers on this branch); it is "A set function applied to such
  a name exhausted memory on the C++ backend before the fix of #97."
  examples/README: the 20 list markers (py, cxx), (py) and (cxx) and the
  words "the backends it runs on" (line 38) are removed, as stage 5 removed
  the "Backends:" lines of docs/source/Examples.rst (the page names a
  backend only for 03 and 04 now; the first report of this entry said the
  lines were moved to the developer notes, which was wrong).  The
  restrictions of EXAMPLES in tests/unit_examples.py are one sentence of the
  paragraph on the test: 03, 11, 12 and 22 run on the C++ backend only, 04
  on the reference backend only.  The words "once per backend" of the
  regeneration instruction moved to tests/README (the bullet on
  unit_examples.py), which names the backends.  Three paragraphs that the
  first pass wrapped at 77 columns are wrapped at the width of their file:
  15 (65) at 76, 18 (101) at 75, 04 (37) at 75.  04 (26), a line of 137
  columns from stage 5 (three sentences on one line), is wrapped at 75 with
  its paragraph; no word changed.  The comment of USER_READMES says that the
  developer files are allowed to name the backend and are not scanned (it
  said they name it; README.contrib, src/README, src/python/README.md and
  tests/data/README do not).
- Verification, in the clean environment (env -i with PATH, HOME, LC_ALL,
  TMPDIR), after the review.  tests/run_tests -j 2 --mem 4 --backend both
  unit_reference_backend.py unit_examples.py unit_api.py: 6 of 6 ok, 0
  failed (unit_reference_backend.py 8 tests on cxx and on py; unit_api.py 12
  tests on each; unit_examples.py 22 tests on py in 87.6 s and on cxx in
  20.0 s), wall 108 s.  make -C docs html: build succeeded, 0 warnings.  No
  expected.out changed; git status names the 17 READMEs, examples/README,
  tests/README, the test and this file; no line of the diff holds a
  machine-specific path or name; every changed README line is within the
  width of its file.

2026-10-09: set functions, the captured arguments and the failure rule
  (decisions D2 and D1 of the memo on the Fair Scheme proofs; #117;
  worktree wt/setfun)
- The owner approved the memo of 2026-10-08 with its recommendations on
  2026-10-09.  Two of its decisions change the semantics of the set
  functions, and this lane implements them on both backends.  D2 (option
  O1): the arguments the function value given to set_k holds are boxed as
  the value arguments are, so their non-determinism escapes the capsule;
  captureS stays the explicit capture.  D1 (option F2, the amendment
  A-fail): a failure that comes from an argument and is demanded inside
  the capsule fails the set function, behind the flag setfunction_failures,
  whose default stays 'encapsulate' in this lane.  D3, the shared capsule
  (#86), is the next lane: nothing here changes the ownership of a capsule,
  and test_shared_capsule keeps its expected-failure mark.
- D2, where the boxing happens.  setN rewrites set_k f a1 .. ak to evalS
  (applyS (.. (set f) ..) ak); applyS boxed each ai (a SetGuard without a
  set, which evalS gives the new set) and set copied the partial
  application f as it was, so a partial application's arguments and the
  variables a lambda closes over (lambda lifting makes them arguments of
  the lifted function) entered the capsule unboxed: a choice or a free
  variable among them was encapsulated when the enclosing context had not
  decided it, and escaped when it had (choice_escapes, the rule of
  2026-10-05).  Now set_step (currylib/setfunctions.cpp) and set_
  (backends/py/currylib/setfunctions.py) put every argument of the partial
  application under a guard without a set, below which the guards crossed
  on the way to the function value stay (guard_successors); evalS_step
  gives the top guard the new set as before.  This is the entry rule of
  the dissertation (chapter 4, p. 158), which boxes a_0 .. a_n, applied to
  the arguments a partial application holds, which the dissertation treats
  as data (p. 153).  What stays unboxed: an argument applied with captureS
  ($<), an encapsulated expression (set0, exprS), and the head of the
  function value.  The rule of choice_escapes for a captured choice an
  enclosing configuration decided stays for them, and holds_private_state
  stays as it was: a captured choice decided before the capsule starts is
  still found by the walk, through the guard, and the capsule is still a
  private copy; inside it the choice now escapes through the guard and the
  split sorts the one configuration to its side (one split and one restart
  instead of the fork in place that pruned it before).  Both readings give
  the values of the test corpus; the benchmarks below show no counter
  moved.
- D2, a captured variable or choice inside a nested data structure of the
  closure (the decision the task left here): the whole captured argument
  node is boxed, as a value argument is, and the box propagates as it does
  for a value argument.  A pattern match passes the box on to the
  component through rvalue (fst p with p = (x, 2) captured: capData).  An
  application of a partial application reached through guards (a captured
  closure, capPartial; a value argument that is a closure, argPartial)
  copied the arguments of the partial application out of the box, so a
  choice inside them was encapsulated; this held for value arguments
  before this lane (argPartial gave ([0, 1], 0) and ([0, 1], 1), values
  outside the reference semantics, the same defect as #117 one level
  down).  apply_step (prelude/apply.cpp) and apply (prelude/apply.py) now
  put the guards crossed on the way to the partial application on each of
  its arguments, in the completed application and in the extended partial
  application (guard_successors in builtins.cpp, the box rule for a
  partial application as data: a reference to a boxed expression is
  boxed); applyS and captureS do the same for the arguments the partial
  application already holds; and ($!), ($!!) and ($##) build their apply
  node from the rvalues of the function and the argument, which kept the
  guards of neither.  These three follow from the box rule and from D2's
  "box the whole argument"; they are listed under the owner's items below
  because they reach a value argument's closure, beyond the two rules as
  stated.
- D1, what a boxed failure is.  The runtime has one failure node, so the
  box flag of the memo is read from the graph: a failure is boxed when the
  path from the root of the configuration to it crosses a set guard.  The
  C++ runtime reads the path from the levels of the scan (the root to the
  redex; Scan::frames) and from the guards of the inductive variable (the
  redex to the position): failure_escapes and fail_capsule in
  state/rts_setfunctions.cpp, called at the two places a step meets a
  failure inside a capsule, hnf and procN of fairscheme.cpp (a failure at
  the root of a configuration, which procD meets, is unboxed, because a
  guard at the root is a T_SETGRD node that procN descends).  The Python
  backend reads the call stack of the configuration (the guards of the
  variable of each hnf
  frame, and the guards of each walk of N: the guards of its variable,
  kept as base_guards of the WalkState, and the set ids it pushed) and
  escape_all, which D sets when it strips a guard of the current set at
  the root: boxed_failure in eval/rts_setfunctions.py, called in D, N and
  hnf.  Under 'escape' a boxed failure returns the new status E_SETFAIL
  (fwd.hpp; the exception E_SETFAIL on the Python backend), which every
  enclosing step hands out as it hands E_TERMINATE out, the nested procD
  yields it, and allValues_step (allValues) rewrites the redex of the set
  function to a failure under the guards of the enclosing sets the
  failure crossed, less the current set (capsule_failure_guards): so an
  enclosing set function whose argument the failure came from fails in
  turn when it demands it (nestFail), and a failure made inside the outer
  function and passed to an inner set function fails the inner one and
  drops the alternative of the outer function (nestFailInner).  The queue
  of the failed capsule is left as it is; nothing runs it again.  Under
  'encapsulate' (the default) every failure drops its alternative as
  before (rule SF.5).  A value the program consumed before the failing
  argument is demanded stays a value under both settings (lazyFirst): the
  set is lazy, as the values of SF.3 are.  The flag is threaded as
  setfunction_strategy is: FLAG_INFO and the docstring of
  interpreter/flags.py, the table of the Python API page, SetFFailures in
  fwd.hpp, a field and a constructor argument of RuntimeState, the enum of
  the bindings (constants.cpp) and the mapping of cyrtbindings/__init__.py;
  the Python backend reads it as rts.setfunction_failures.
  unit_cxx_rotation.py constructs RuntimeStateBase directly and gained the
  argument.
- The goals of the memo's table (tests/data/curry/SetFunctionsSemantics.
  curry, with the helpers of the probe modules), measured on this build
  before and after the change, both backends, step mode, prlimit 2 GB.
  Before, on both backends: s6PL [10, 11]; ord2 ([10, 11], 0) and
  ([10, 11], 1); cin1 ([True], _a); cin1Rev (_a, [True]); capNarrow
  ([False, True], _a); capData and capPartial ([10, 11], 0) and ([10, 11],
  1); argPartial ([0, 1], 0) and ([0, 1], 1); s3 []; s3e True; s4 and s4h
  []; s4a, s4b, s4c True; nestFail False; nestFailInner False; lazyFirst
  True.  After, under both settings of the flag: s6PL [10] and [11]; ord2
  and ord2Arg ([10], 0) and ([11], 1); cin1 ([True], True) and ([],
  False); cin1Rev (True, [True]) and (False, []); capNarrow ([False],
  True) and ([True], False); capData, capPartial ([10], 0) and ([11], 1);
  argPartial ([0], 0) and ([1], 1); capNested [[1]] and [[2]].  After,
  under 'encapsulate': s3 [], s3e True, s4 and s4h [], s4a, s4b, s4c True,
  nestFail False, nestFailInner False; under 'escape': s3, s3e, s4, s4h,
  s4a, s4b, s4c and nestFail no value, nestFailInner True; lazyFirst True
  under both.  Unchanged under both settings, on both backends: s1 [-1, 1]
  and [0, 2]; s2 of PG ([1], 0) and ([2], 1); s2 of PL [1]; s3 of PG [1];
  s3i True; s4d True; s5 [[-1, 1]] and [[0, 2]]; nest2 [[False]] and
  [[True]]; s6 of PG [False] and [True]; s7 of PG ([2], True) and ([1],
  False); s7 of PL ([1], True) and ([2], False); s8 of PL [10] and [11];
  s9 [1] and []; s10 [1] and []; ho1 [-1, 1] and [0]; cap1 [-1, 0, 1, 2];
  ord1 (0, [10]) and (1, [11]); cin2 ([True], True) and ([], False); cin3
  ([_a], _a).  Every value after is the reference value of the table
  (sections 5.2 and 5.3 of the memo).  The two backends agree on every
  goal but capNested and nestFail on the Python backend, which assert
  before and after: a guard of an enclosing set at the root of a nested
  configuration, the known failure of that backend.
- Tests: tests/unit_setfunctions_semantics.py, one test per goal above (39
  per backend), each class with one child per setting of the flag; the two
  goals of the known failure are expected failures on the Python backend.
  The existing set-function tests are unchanged and pass: the goldens of
  applyS.curry (goal10, goal11: a captured occurrence applied with $<
  follows the guarded one) and the captured shapes of
  unit_setfunctions_bugs.py keep their values, because a captured
  occurrence of a decided choice escapes now where the private capsule
  forked on it before, with the same values.  Through the runner (-j 2
  --mem 4 --backend both, env -i, after --prepare-only, which compiled the
  products of 24 directories in 477 s after the header change):
  unit_setfunctions_semantics, unit_setfunctions_bugs,
  unit_setfunctions_bindings, unit_cxx_scheduler, func_setfunctions,
  unit_funcpat_narrowed, unit_funpat_free_argument, unit_cxx_freevars,
  unit_py_residuation, unit_cxx_interp, func_funpat, unit_cxx_partial,
  unit_cxx_rotation, unit_cxx_steplimit, unit_py_fairness, unit_py_copy,
  unit_inspect, unit_cxx_rewrite, unit_cxx_literals, unit_cxx_counters,
  unit_cxx_variable, unit_cxx_passthrough, unit_cxx_stack, unit_cxx_heap,
  unit_cxx_gc_counters, func_kiel, unit_cxx_bom, unit_cxx_mps: 56 of 56
  run, wall 100 s, one failure, test_zero_quantum_is_an_error of
  unit_cxx_rotation.py (the direct construction, fixed; 20 tests pass
  alone); unit_examples, unit_cxx_gc, unit_typed_expr, unit_loadsave,
  unit_currylib, unit_cache, unit_importer, unit_sigtable, unit_icurry,
  unit_flat2icurry: 20 of 20 run, wall 206 s, two failures, unit_currylib
  on both backends (below; 10 tests pass on each after the fix);
  func_docs 3 tests on each backend; make -C docs html: build succeeded,
  no warning on the edited pages.
- The library source.  The comments of set and applyS in
  curry/lib/Control/SetFunctions.curry describe the two rules.  The module
  is one of the eight whose source tests/unit_currylib.py pins by digest,
  and a changed time stamp made make install of test_install_interfaces
  derive the JSON again from the committed .icy (identical bytes).  As the
  README of the library prescribes: the .icy made from the edited source
  with sprite-make --icy is byte-identical to the committed one, and the
  digest in unit_currylib.py is updated.
- Cost of D2 (run_benchmarks, C++ backend, one repetition, step mode, the
  machine shared with two other workflows; compare --deterministic): the
  steps, forks and collections of QueensSet, QueensSet9 and Queens10 are
  equal before and after; the instructions of QueensSet9 18773.4 M to
  18779.7 M (+0.03 percent) and of Queens10 9542.1 M to 9577.0 M (+0.4
  percent, within the tolerance); QueensSet 6317.1 M to 3702.4 M, where
  the count before included the background compile of the module after
  the fresh checkout of the worktree.  The ~cpu and ~wall ratios of 1.6
  are the load of the machine (advisory).  The set functions of the three
  programs pass plain functions (set1 unsafe, set0 goal), so no argument
  is boxed by the new rule, and no captured choice splits a capsule.
  PokerChoice and PokerFree did not run before or after: their source
  needs the Curry preprocessor (currypp), which the PAKCS installation of
  the machine lacks.
- Left to the owner.  (1) D3, the shared capsule (#86, route (c) decided):
  the next lane; test_shared_capsule stays a known failure.  (2) The
  default of setfunction_failures: 'encapsulate' here; 'escape' after the
  differential harness confirms it, as the memo says.  (3) The three
  propagation changes under D2 above (apply, applyS and captureS, and the
  strict applications) box the arguments of a closure passed as a value
  argument as well: argPartial changed from ([0, 1], 0) and ([0, 1], 1) to
  ([0], 0) and ([1], 1), the reference values.  (4) holds_private_state
  still copies the capsule for a captured choice decided before the start,
  where the escape now handles it; the walk could skip the guarded
  subtrees, which would leave such capsules shared (no benchmark counter
  moved either way).  (5) Under 'escape' a set consumed in part keeps the
  values produced before the failing argument is demanded (lazyFirst,
  select): the lazy strategy, not the semantics of CHRS13, which has no
  value for the whole set.  (6) The Python backend's known failure on a
  guard of an enclosing set at the root now covers capNested and
  nestFail as well.  (7) tests/manifest.json has no entry for the new file
  (a calibration run on a quiet machine adds it).  (8) Issue #117 gets its
  closing comment when the commit is merged and CI is green; nothing is
  committed from this lane.
- The review of the lane.  (a) A PartialS reached through a box, applied
  with applyS inside the capsule (pS, pSCap in the test module), crashed
  the C++ runtime (a null set in update_escape_sets) and raised KeyError on
  the Python backend: applyS and captureS put the crossed guards around
  the box without a set that set_step had made, so the box was no longer a
  direct successor of the goal, and evalS never gave it the new set.
  guard_successors (builtins.cpp) and the new guard_held
  (eval/rts_setfunctions.py) keep such a box outermost and put the guards
  on its value.  pS gives [[1]] and [[2]], pSCap ([[1]], 0) and
  ([[2]], 1), the reference values, on both backends under both settings;
  the two goals are tests.  (b) The eager strategy
  (setfunction_strategy 'eager') loops on s1 and on s6PL, before this lane
  as well (eagerApplyS_step makes a partial application of itself, so the
  normalized argument comes back to the same step); s2PL answers with no
  value.  The two rules were not exercised under the eager strategy.  The
  fix is the owner's (the strategy has one branch in func_setfunctions.py
  and the documentation still names it).  (c) The comment of
  guard_successors said an allocation may move the node; this heap never
  moves a node (graph/memory.hpp).  The comment now says why the slot is
  read again.
- The suite stage (2026-10-09).  The prepare pass, the fast selection
  (124 file runs), the set-function and capsule files (46 file runs), the
  three set-function files under interpret:off and interpret:all, and the
  oracle suites (44 file runs) passed on both backends.  The docs build
  reported two docutils errors from the docstring of guard_held (a word
  that ends in an underscore reads as a reference); the docstring is
  fixed, and the build has no warning.  The counters of Tak1 and Fib equal
  the committed record of 2026-10-04; the steps of Queens10 (17706900)
  differ from that record (54505073) on the clean head of py314 as well,
  so the difference predates this lane (the rotation in step mode of
  2026-10-05 came after the record).  The 18 unit files outside the fast
  selection and the set-function batch ran as a third batch (36 file
  runs, no failure), so every unit and functional file ran on both
  backends in this lane, in four bounded runs rather than one.

2026-10-09: the shared capsule is cloned at its first divergence (decision D3
  of the memo on the Fair Scheme proofs, route (c); #86; worktree wt/capsule)
- The defect (the known failure test_shared_capsule, the owner's item of the
  entry of 2026-10-07).  A capsule started while a variable of its goal was
  free, consumed in part, and read again by two configurations that bound
  the variable differently after the start: the nested evaluation read the
  binding of the first reader through the queue stack (the fix of #97), put
  it into the nested spine, and consed the value onto the shared allValues
  node, which the other reader then took.  sharedCase and sharedEq gave
  (0, 1) twice where the plain goals give (0, 1) and (0, 2); sharedPat gave
  (0, 1) twice where the plain goal gives it once.
- The decision.  The owner approved the memo with its recommendations on
  2026-10-09: route (c), clone the capsule at the first divergence of the
  enclosing state, or index its nested state by that state; route (b), an
  ownership test with the narrowing route (a) for a shared capsule, as the
  fallback when (c) costs too much.  The bound for "too much", set before
  the measurements: a change of the steps or the forks of QueensSet, of
  QueensSet9 or of the shape of unit_funcpat_narrowed.py (a goal without a
  shared capsule pays nothing), or more than twice the steps of the private
  shapes (privateCase, privateEq) on the shared ones.  Route (c) is within
  the bound (below); route (b) was not needed.
- The rule, operationally.  A divergence is the first read, by the nested
  evaluation of a capsule, of a binding that the configuration which runs
  the capsule holds and the capsule does not.  The bindings are the one
  kind of enclosing state that enters a capsule without a choice node: a
  decision of a choice of the goal and a narrowing of a variable of it
  escape as choices and split the queue (the repair of #61), and a group
  made outside is restored at the escape.  get_binding (rts_freevars.cpp;
  _find_binding in rts_bindings.py) reports the level at which it found
  the binding: 0 for the map of the configuration or the bindings its
  queue absorbed (Queue::absorbed, new; Queue.absorbed), k for the k-th
  enclosing level.  A reader that puts a binding of a level above into the
  evaluation (replace_freevar, both overloads, and the walk of evalS_step;
  N, hnf, D and the walk on the Python backend) returns E_DIVERGE
  (RuntimeState::diverge; the exception E_DIVERGE), which every step hands
  out as it hands E_UNWIND out, up to the allValues_step of the
  configuration that holds the binding.  That step clones the queue
  (Queue::clone: the same configurations, shared and cloned before a step
  as after a split; the decisions; the absorbed bindings, plus the one
  read, under the id the configurations of the queue use), puts allValues
  of a new SetEval over the clone into a private copy of its spine, as a
  binding itself goes into an expression, and restarts.  The read then
  finds the binding at level 0.  The shared node stays for the other
  configurations, each of which clones the capsule at its own first read.
  Nested capsules diverge from the outside in: the outer queue is cloned
  for the reader, and at the next read the inner queue for the clone's
  configuration (sharedNested).  Two bindings cost two clones (sharedTwo).
  The clone is the index of route (c) made lazily: one nested state per
  enclosing state the capsule read.  The steps in between are left as
  they were, with their queues, as after E_UNWIND (procD sets
  forced_rotate); the collectors mark the absorbed bindings as roots.
- Three shortcuts keep the cost off goals without sharing.  The walk of
  evalS_step (private_state, which replaced holds_private_state) absorbs
  the bindings it meets into the new queue, so a capsule created after the
  binding never diverges on it.  A binding found at the outermost
  configuration while it is alone in its queue is absorbed in place, with
  no clone and no restart: no other configuration reaches the capsule, and
  a later clone of the configuration starts with the same binding (the
  reasoning of the shortcut of the walk).  Without it the last alternative
  alive, whose walk the shortcut skips, cloned the capsule at its first
  read and lost the sharing of the capsule between the two paths of
  twoValues (privateCase went from 110 to 137 steps on the C++ backend;
  back to 110 with it).  The outermost configuration alone in its queue
  takes no private copy, and it skips the walk when it holds no binding;
  when it holds one, the walk runs to absorb alone (the review below).
- Sources.  C++: fwd.hpp (E_DIVERGE), state/queue.hpp and queue.cpp
  (absorbed; clone; split copies absorbed), state/rts.hpp (diverge_queue,
  diverge_vid, diverge_binding; get_binding with the level; diverge),
  state/rts_freevars.cpp, state/rts_setfunctions.cpp, fairscheme.cpp (the
  case in procD and in procN), currylib/setfunctions.cpp (allValues_step,
  private_state, evalS_step), graph/gc/wdgc.cpp and mps.cpp.  Python:
  generic/eval/control.py (E_DIVERGE), eval/queue.py, eval/configuration.py
  (share: the clone copies the configurations, since the queues of this
  backend hold references), eval/rts_bindings.py, eval/rts_setfunctions.py
  (clone_queue), eval/rts.py, currylib/setfunctions.py.  The primer's
  section on set functions and the developer notes on the Python backend
  describe the rule; the note on the idiom of #86 is unchanged (below).
- Tests.  tests/unit_setfunctions_bindings.py: test_shared_capsule is a
  plain test, and every test of a shared shape runs its child under both
  settings of setfunction_failures.  Section 6 of
  tests/data/curry/SetFunctionsBindings.curry adds a shape per case the
  rule covers, each with its plain goal: the binding of =:= (sharedStrict),
  a data type narrowed after the start (sharedNarrow: the escape route, no
  clone), a functional pattern applied to the shared variable after the
  start (sharedFunPat: id "xa" against the free s narrows its spine and
  binds its characters), nested capsules over one variable (sharedNested),
  two variables (sharedTwo), and one alternative alone (soleReader, the
  in-place absorption).  PAKCS 3.4.1 on the plain goals, in a module
  without the set functions: plainStrict and plainNarrow (0,1) (0,2),
  plainFunPat (0,2), plainTwo (0,11) (0,12) (0,21) (0,22), plainSole
  (0,1); Sprite gives the same on both backends.
- Measured (step mode; the machine shared with other work).  The shared
  goals, C++ backend, steps/forks before and after: sharedCase 29/2 to
  43/2, sharedEq 29/2 to 39/2, sharedPat 76/7 to 113/9 (the forks of
  plainPat); Python: 25/2 to 33/2, 25/2 to 30/2, 55/7 to 74/9.  The
  private shapes are unchanged (C++ 110/4 and 106/4; Python 91/4 and
  88/4).  The new shapes, C++: sharedStrict 45/2 (plainStrict 23/1),
  sharedNarrow 42/7 (18/5), sharedFunPat 298/40 (208/36), sharedNested
  74/2, sharedTwo 135/4 (91/3), soleReader 24/1.  One clone per reader
  that diverges, and the last reader alive absorbs in place (a probe that
  counts clone_queue on the Python backend: one clone each for
  sharedCase, sharedEq and sharedPat, none for the private shapes).  The
  counters of unit_funcpat_narrowed.py are equal on both backends (C++:
  one 145, 138 and 162 steps at 8 forks; many 219524/1202, 12102/8,
  177745/1202; distinct 9529/324, 1690/72, 10231/450; Python: many
  12867/1202, 12882/8, 13468/1202; distinct 1333/324, 1504/72, 1594/450):
  the shape has no enclosing binding, so its pinned shape does not flip,
  and the primer's note on the idiom stands.  run_benchmarks -r 1 -b cxx
  --no-perf, compare --deterministic: QueensSet 2089740 steps, 511430
  forks, 3 collections, and QueensSet9 11784795, 2978740, 11, equal
  before and after; the ~wall ratios 1.32 and 1.23 are the load of the
  machine (advisory).
- Verification.  The single files, one process at a time, both backends:
  unit_setfunctions_bindings.py (13 tests), unit_setfunctions_semantics.py
  (the 41 goals of the memo under both settings; the Python backend keeps
  its two known failures), unit_funcpat_narrowed.py, unit_setfunctions_bugs.py
  (22, one skipped; the Python backend keeps its known failure),
  unit_cxx_scheduler.py: all pass; unit_setfunctions_bindings.py passes in
  the stress mode of the collector on the C++ backend (the absorbed
  bindings are roots).  Through the runner (-j 2 --mem 4 --backend both):
  unit_setfunctions_bindings.py, unit_setfunctions_bugs.py,
  unit_setfunctions_semantics.py, unit_cxx_scheduler.py,
  func_setfunctions.py, unit_funcpat_narrowed.py,
  unit_funpat_free_argument.py, unit_cxx_freevars.py,
  unit_py_residuation.py and unit_cxx_counters.py: 20 of 20 run, 0 failed,
  wall 46 s; unit_cxx_gc.py, unit_cxx_gc_counters.py, unit_cxx_mps.py,
  unit_cxx_heap.py, unit_cxx_interp.py, func_funpat.py, func_kiel.py,
  unit_examples.py and unit_inspect.py: 18 of 18 run, 2 failed, wall
  217 s, the two on the C++ backend for the stale products of the
  runtime-header change (unit_cxx_heap.py found no Tak1.so; unit_inspect.py
  found Peano interpreted); after --prepare-only (12 directories, 465 s)
  the two files pass on both backends (4 of 4).  Not run: the full suite.
- Review of 2026-10-09 (three findings, applied in this worktree).  (i)
  Under the Memory Pool System back end (make GC=mps) the map of the walk
  held the addresses of the bindings across the allocations of evalS_step
  (the goal, the guards) before the new queue took it, and the map is no
  root: a node moved there left a forwarding object in the map.  Not
  observed (the installs use the default collector, which collects at
  the safepoint alone); evalS_step holds a GcClamp from the walk to the
  swap, the rule of gc/mps.cpp for a holder of node addresses across
  allocations.  (ii) A binding that predates the capsule, made by the
  outermost configuration while alone in its queue, was not absorbed at
  the creation (the shortcut skipped the walk), so after a later fork on
  something else each alternative alive at its first read cloned the
  capsule and redid the nested work: boundThenFork, values 1 and 11, went
  from 32 to 40 steps on the C++ backend against the base install (30 to
  33 on the Python backend), with one clone.  Now the walk runs for such a
  configuration when it holds a binding, to absorb alone (no private copy;
  a configuration without a binding still skips it): boundThenFork is back
  to 32/2 and 30/2 with no clone, and every other shape measured is
  unchanged.  (iii) One alternative that reaches the shared capsule
  through two references clones it twice, once per reference, since the
  clone goes into a private copy of the spine of the reference that read
  the binding and the shared node stays for the other: twoRefs gives
  (0, 11) and (0, 22), right, at 63 steps on the C++ backend against 46
  for plainTwoRefs (Python 45 against 24), two clones.  The remedy is the
  per-configuration map from a copied node to its copy that the comments
  of #86 name, the owner's design decision; the shape is pinned with its
  values and its clone count (item 9 below).  Tests: test_clone_per_reader
  (one clone per shared shape of section 5, none for privateCase),
  test_binding_before_fork (no clone) and test_two_references (two), with
  the clone count checked on the Python backend by a child that counts
  clone_queue (the C++ runtime has no counter of the clones); the file has
  16 tests, which pass on both backends and in the stress mode of the
  collector.  The counters of unit_funcpat_narrowed.py and the benchmarks
  are equal again (QueensSet 2089740 steps, QueensSet9 11784795, compare
  --deterministic: counters equal; the walk of (ii) runs only for a
  configuration with a binding, which these programs never have at the
  top level).
- The suite stage (2026-10-09, after the review; the machine shared).  make
  stage and make overlay: exit 0.  ./run_tests --prepare-only --backend
  both -j 2 --mem 4: 24 of 24, 0 failed, 41.5 s.  --changed selects the
  whole suite (116 files: the runtime header and control.py changed), so
  --fast --backend both ran instead: 124 of 124, 0 failed, 120 s; the
  set-function and functional-pattern files (unit_setfunctions_bindings.py,
  unit_setfunctions_bugs.py, unit_setfunctions_semantics.py,
  func_setfunctions.py, unit_funcpat_narrowed.py,
  unit_funpat_free_argument.py, func_funpat.py): both backends 14 of 14,
  0 failed; interpret:off 7 of 7; interpret:all 7 of 7 (87 s).  The oracle
  suites 'func_*.py', both backends: 44 of 44, 0 failed, 73.5 s.  make -C
  docs html: build succeeded, 0 warnings.  run_benchmarks -r 1 -b cxx
  --no-perf: QueensSet 2089740 steps, QueensSet9 11784795, Queens10
  17706900, Tak1 33629915; compare --deterministic against the record of
  2026-10-04 flags Queens10 (54505073 before the step-mode rotation) and
  QueensSet, QueensSet9 (-2.9%, -3.0%), whose counters equal the record
  taken on the clean merge base before this change (counters equal); Tak1
  equal.  The full unit suite ('unit_*.py', both backends, 188 runs) was
  started through the runner and cut short by the end of the stage at 3 of
  188 runs, all ok; it remains to be run (--prepare first).
- Left to the owner.  (1) The absorbed map is keyed by the group id of
  the variable in the configurations of the queue, as the maps of the
  configurations are; a group made inside the capsule after the
  absorption (x =:= y between two variables of the goal) is not followed,
  as the walk through the enclosing maps did not follow it before.  (2)
  apply_binding reads the map of the configuration alone, as before: a
  nested fork on the generator of a variable whose binding the queue
  absorbed applies nothing and relies on the escape of the generator to
  the configuration that holds the binding; the case of a captured
  variable narrowed by a sibling alternative was not constructed.  (3)
  The Python clone copies the configurations at once, where the C++
  shares them and clones one before a step; the Python queues hold
  references after a split as well (the reader's table of differences).
  (4) A capsule whose argument exceeds the budget of the walk is private
  at creation and absorbs a binding beyond the budget at its first read,
  through a clone when the outer queue holds more than one configuration.
  (5) tests/manifest.json: the entry of unit_setfunctions_bindings.py
  predates its 16 tests (a calibration run on a quiet machine refreshes
  it).  (6) Issue #86 keeps its item on the cost of a narrowed name (the
  restart per read), which this lane does not change; the comment on the
  shared capsule waits for the merge and CI.  (7) The notes of the memo
  name holds_private_state, which is now private_state.  (8) Nothing is
  committed from this lane.  (9) The clone per reference (finding (iii)
  of the review): one alternative that reads the capsule through two
  references clones it twice and does the nested work after the clone
  point per clone; the per-configuration map of #86 (the SetEval node to
  its clone for the configuration) would make it one.

2026-10-09: the checker of memo 6.5 (the run-time invariants of the Fair
  Scheme, first in the Python backend; worktree wt/checker)
- The task.  Section 6.5 of the memo on the Fair Scheme proofs asks for a
  checker mode, first in the Python backend, where instrumentation is cheap,
  then mirrored in the C++ runtime behind a flag (a later lane).  The owner
  approved the memo with its recommendations on 2026-10-09.  This entry
  delivers the Python checker: the flag, the checker object, its hooks, the
  checks of parts (1) to (5) of the lane, the tests, the cost, and the
  violations found.
- The flag.  ``checker`` (True | False, default False) in
  curry.interpreter.flags, documented with the other flags, in the flags
  table of the Python API page and in a section of the developer notes.
  The exact text for a run: SPRITE_INTERPRETER_FLAGS=backend:py,checker:true
  (the harness system py-check is the pair ('backend:py,checker:true', '')
  in the systems table of run.py).  The C++ backend accepts the flag and
  ignores it.
- The design.  RuntimeState.checker holds one Checker
  (backends/py/eval/checker.py) while the flag is on and None otherwise.
  Every hook in the runtime is one test of that attribute, so the flag-off
  path takes the same steps and makes the same nodes as before.  The
  checker observes: it never writes the state (it reads the union-find
  without path compression, the bindings without the absorption of
  _find_binding, and the generators without their creation).  A violation
  raises InvariantViolation, an AssertionError whose message names the
  invariant, the event, the detail, the identifiers, the configuration (its
  queue, its set, its index, escape_all, the fingerprint, the groups, the
  goal position from the call stack, the root expression) and, for a
  dispatch-chain disagreement, the enclosing configuration.
- The hooks (the four events the memo names, and the three the checks of
  FS-x and B2 need).  fork: rts_fingerprint.fork, after the clones are made
  (the generator collects them and calls the hook when the queue has taken
  them).  escape: rts_setfunctions.split_queue (rule SF.1), with the
  configurations of the queue before the split; update_escape_set, the
  insertion of an identifier by a pull-tab across a box.  pull-tab:
  rts_fingerprint.pull_tab, once before the rewrite with the two copies and
  once after it with the created choice.  yield: rts_control.release_value,
  before the value is made.  instantiation: rts_freevars.instantiate (rule
  S.x), before and after the write; rts_freevars._make_generator; the
  private copies of rule N.x in N (three sites) and hnf (the binding), and
  the value bindings of a builtin type in hnf.  step: fairscheme.S after a
  completed step, with the symbol and the successors the redex held before
  (the list object is kept, not copied: a step replaces it).  hnf: at the
  entry of fairscheme.hnf.  capsule entry: rts_setfunctions.create_queue.
  freshvar for the tags of a variable.
- The checks.  (2) B1: at every fork, escape and yield the fingerprint of
  each configuration involved is a function and its group invariant holds
  (every decided identifier agrees with the root of its group in the
  union-find of the configuration, and no member disagrees), and the
  fingerprints along the dispatch chain agree (def:s-chain of the note:
  the enclosing fingerprint is the union); each clone of a fork keeps
  every decision of its parent (read through the clone's groups), has the
  decision of the fork, in its own fingerprint or in an enclosing one, and
  adds at most the forked identifier and its group root (rule D.2; a fork
  on a free variable applies its bindings and equates its group, so the
  last clause is skipped for it); at most two clones with different
  sides.  A pull-tab gives the created choice the identifier of its
  source, the source is at the recorded path, the two alternatives are
  fresh copies of the spine that share the cells off the path and end in
  the alternative, and the identifier is in the escape set of every box
  the real path from the root to the source crosses (def:s-pull; the
  insertions precede the pull-tab in N and hnf, so the check is exact
  there, where the dropped-box defect below is found through the path
  alone).  At a yield no choice, operation,
  failure or constraint cell is reachable from the value; the generator of
  a free variable and the arguments of a partial application are not
  entered.  (3) FS-x.  X1: one generator per variable (the table of the
  checker), its root a choice with the variable's identifier, checked at
  the creation and at every instantiation.  X-c at a yield: no variable
  with a binding or a decision remains in the value, and for every
  variable of the table with a generator and a decided root the
  identifiers on the decided path of its generator are decided.  X-b' at
  an instantiation: the slot held the variable, the write put its
  generator there, and the reduct of every configuration of every queue
  that references the redex, read under the fingerprint it is compatible
  with, is the same before and after the write; after a copy of rule N.x
  the copy is a faithful copy of the spine with the replacement at its end
  and its reduct equals the reduct of the root it replaces.  The reduct is
  read with the generator image of the memo's revision: a decided choice
  is its side, a bound variable its binding or its generator, a free
  variable and an undecided generator root the group of the variable, and
  a forward node or a box is passed through; it is compared as a digest
  (blake2b over the cells), within a budget of 5000 cells per event; an
  event over the budget is counted (counts['over_budget']) and not
  compared.  None of the suite's goals reached the budget.  (4) FS-S.
  Every identifier is tagged at its creation with the boxes above it: the
  set guards crossed from the root of each configuration of the dispatch
  chain to the redex (the frames of the call stacks, as boxed_failure reads
  them, and escape_all), plus the flags of the redex cell.  The flags are
  the entry step of def:s-flags: at the creation of a capsule the cells
  reachable from its boxed arguments are flagged with the set (a bounded
  walk; a capsule beyond the budget turns its S0 checks off), and the
  cells a step, a pull-tab, a copy or a generator creates at a flagged
  cell inherit the flags (a bounded walk from the redex that stops at
  flagged cells).  A generator inherits the tags of its variable.  The
  tags define "argument-derived".  S0: an insertion into an escape set is
  of an argument-derived identifier (E in A); a fork inside a capsule is on
  an identifier that is function-derived for the capsule (its tags), that
  is in the escape set of no enclosing capsule (the shadow sets: the tags
  of an enclosing set over-approximate its escape set, since a choice a
  function makes inside the inner capsule is created under the box of the
  outer one; argAfterStart of SetFunctionsBugs is the case) and that no
  enclosing configuration decided, or on one the queue was split on
  before (the three clauses of def:s-escapes, as choice_escapes reads
  them);
  at SF.1 the escaping identifier is argument-derived for the capsule or an
  enclosing one of the chain, or an enclosing configuration decided it (the
  three clauses of def:s-escapes, read on the tags instead of the escape
  sets).  The escape sets only grow (a shadow per set; a loss and an
  insertion outside the rule are both reported).  S-split: after SF.1 the
  kept queue holds the LEFT and the undecided configurations, the new one
  the RIGHT and the undecided, nothing else and nothing lost, both record
  the identifier, both name one set.  (5) B2.  The definitional tree of an
  operation is rebuilt from the ICurry case structure of its body (the
  positions of the case variables through the IVarAccess assignments; a
  case on a variable that is not a position is left untracked), found
  through the symbol tables of the loaded modules by the info table of the
  redex.  At every hnf the position is a case of the tree of the operation
  at the redex whose branches above it match the constructors at their
  positions (B2 a, b).  After a step that replaced the redex by a failure
  the matched path ends in an exempt leaf, or in a literal case without a
  branch for the value (B2 c); a failure that hnf writes at an inductive
  position never reaches the hook (the step unwinds).  A built-in has no
  tree and is not checked; the Prelude functions with ICurry bodies are.
- The tests (tests/unit_py_checker.py, 38 tests; 35 skip on the C++
  backend).  TestKnownGood: the goals of the probe table under both failure
  rules, the goals of SetFunctionsBindings, the goals of
  unit_setfunctions_bugs.py, the functional-pattern modules, the goals of
  unit_cxx_freevars.py, and a module with the narrowed name of
  unit_funcpat_narrowed.py, a released residual, a narrowing and a pairing:
  each runs under the checker without a report and with the values it has
  without the flag (both runs in the test).  One constructed violation per
  check: the group invariant at a fork (a decided member of a group whose
  root is undecided), a clone that drops the fingerprint of its parent
  (Configuration.clone replaced in the test: without the check the goal
  dup2 gives 16 values for 4), the identifier and the copies of a
  pull-tab, a pull-tab across a box without the insertion into the escape
  set, a choice in a yielded value, a nested configuration against its enclosing one,
  a second generator and a wrong root identifier, a decided variable in a
  value, an undecided inner identifier, a copy whose reduct changed, an
  instantiation whose copy is corrupted off its path (copy_spine replaced
  in the test), an insertion of a function-derived identifier into an
  escape set, a fork inside a capsule on an argument-derived identifier, a
  fork inside a nested capsule on an identifier in the escape set of the
  enclosing set and one on an identifier the enclosing configuration
  decided (choice_escapes turned off for the state, since the runtime
  would split the queue), a split on a function-derived identifier, a split that keeps a decided
  configuration in both queues, an escape set that lost an element, a step
  that demands a position before the inductive position (the step function
  replaced in the test), a step that fails on a matched rule, and the flag
  itself.  TestDroppedBox pins the defect below (two known failures).
- The cost with the flag off.  The steps and the forks of the goals of
  unit_funcpat_narrowed.py are unchanged: one/source 107 steps 8 forks,
  many/source 12867 and 1202, distinct/source 1333 and 324, one/sourceS 122
  and 8, many/sourceS 12882 and 8, distinct/sourceS 1504 and 72,
  one/sourceN 111 and 8, many/sourceN 13468 and 1202, distinct/sourceN
  1594 and 450, before and after (and the same under the checker).  The
  wall time of Half.curry main on the Python backend (goal2, toPeano 1600):
  35.9, 35.2, 36.1 s before, 36.6, 36.4, 36.4 s after, on a shared machine:
  the mean rises 2 percent, at the edge of the spread of the runs; the
  flag-off path has one attribute test per step, per hnf, per pull-tab,
  per fork, per yield and per copy (fork allocates its list of clones
  under the test as well).  With the flag on the funcpat goals take 10.0 s
  for 4.8 s, and the checker holds memory as well as time: it keeps every
  flagged cell alive (the entry walk flags up to 50000 cells per capsule,
  a step or a copy up to 1024) for the life of the runtime state, so a
  long run of a capsule under the checker may need a larger address limit
  than the plain run (HARNESS_AS_LIMIT of the harness, 2 GiB by default).
- The runs.  The seven files of the lane under the checker through the
  runner (-j 2 --mem 4 --backend py, SPRITE_INTERPRETER_FLAGS=
  backend:py,checker:true): unit_funcpat_narrowed.py 2 of 2,
  unit_setfunctions_semantics.py 41 of 41 (both failure rules through its
  children, which inherit the flag), unit_cxx_freevars.py 7 of 7 (its
  Python half), unit_setfunctions_bindings.py 16 of 16,
  unit_py_residuation.py 6 of 6, unit_funpat_free_argument.py 9 of 9, and
  unit_setfunctions_bugs.py 21 of 22: test_captured_then_guarded reports
  the defect below; 34 s wall.  With the flag off the unit_py_* files, the
  seven files and the new file: 21 of 21 files, 0 failed, 39 s wall.  The
  new file on the C++ backend: 34 tests, 31 skipped, ok.  make -C docs
  html: build succeeded.
- The violations found.  (1) A true defect of the Python backend, found at
  the pull-tab check (B1: the source is not at the recorded path): in the
  walk of N a forward node at the cursor is replaced by
  indexing.logical_subexpr(parent, index, update_fwd_nodes=True), which
  skips the forward chain and the set guards behind it, while the real
  path of the walk and its list of set ids stay as they were.  A choice
  behind a box behind a forward node is then pull-tabbed along a path that
  omits the box: the copies of the spine lose the guard, and the escape
  set of the capsule is not updated.  A captured choice reached through a
  forward node alone is encapsulated.  Reproducer (data T = A | B;
  constT t _ = t; pickOne z f = [z] ? [f 0]; oneCaptured = let x = A ? B in
  let vs = sortValues (set2 pickOne A (constT x)) in (length vs, x)): the
  Python backend gives (3, A) and (3, B), the C++ backend (2, A) and
  (2, B), the values of weakly encapsulated search.  The goal
  capturedThenGuarded of unit_setfunctions_bugs.py has the same walk and
  passes by the second, boxed occurrence of the choice, which escapes and
  sorts the configurations forked too early.  The fix is in the T_FWD case
  of N (fairscheme.py): step through the forward chain and the guards one
  node at a time, with the real path and the set ids extended as the walk
  does for a guard at the cursor; not made in this lane.  Pinned in
  TestDroppedBox (test_values, a known failure on the Python backend;
  test_under_the_checker, a known failure; test_report, the message).  (2)
  A false alarm, fixed in the checker: the first form of the fork check
  required the clone's own fingerprint to hold the decision; a nested fork
  on an identifier an enclosing configuration decided writes nothing
  (update_fp reads the decision through the queue stack and keeps one
  side).  The check reads the decision through the chain.  No other report
  on the goals of the lane.
- The review of the lane (2026-10-09, seven findings) and what was done.
  (1) The fork check did not relate the clone to its parent (rule D.2):
  confirmed with Configuration.clone replaced to give an empty
  fingerprint, dup2 then gives 16 values with no report; the check
  _check_clone above was added, with the test.  (2) sprite-exec prints an
  InvariantViolation, an AssertionError, as "an internal error occurred.
  Rerun with SPRITE_DEBUG=1 for details." (ProgramErrorHandler.print_error
  of curry.tools.utility): confirmed on the reproducer, with and without
  SPRITE_DEBUG=1.  The output of sprite-exec is the owner's; the report of
  a violation needs SPRITE_DEBUG=1 under sprite-exec today, and the
  developer notes say so.  (3) B2 is asserted for the operations with an
  ICurry body alone: on the set-function and functional-pattern goals of
  the suite 10773 of 16492 hnf calls are untracked built-ins (plusInt
  4947, nonstrictEq 1555, apply 771, cond 583, eqChar 552, allValues 442,
  _biString 397, & 397, constrEq 390, ltEqInt 140, evalS 122, applyS 118,
  set 104, $! 84, eqInt 82 lead); stated in the notes and here; no
  hand-written tree table for the strict built-ins was added.  (4) The S0
  fork check read the tags of the current set alone: the three clauses of
  def:s-escapes are checked now (see the checks above).  A first form that
  read the tags of the enclosing sets reported argAfterStart, a false
  alarm: the tags over-approximate the escape sets of the enclosing sets.
  (5) The yield walk does not enter the arguments of a partial application
  (pa = (+) (1 ? 2) yields a function value with the choice inside it, no
  report), nor the generator of a free variable: stated in the notes and
  here as a reading the paper must carry; whether the model's value
  definition treats a partial application as data is the owner's
  question.  (6) fork allocated its list of clones with the flag off:
  guarded now; the notes say "one attribute test per event" and name the
  memory the checker holds.  The steps and the forks of the funcpat goals
  are the same as before, flag off and on.  (7) The pull-tab check did not
  assert the insertions of def:s-pull: added (_check_crossed_boxes), with
  the test.  After the changes: unit_py_checker.py 38 tests ok (2 expected
  failures) on the Python backend and ok (35 skipped) on the C++ backend;
  the seven files under the checker through the runner: 7 of 7 run, 1
  failed (test_captured_then_guarded, the dropped-box defect), 32 s wall.
- Left to the owner.  (1) The C++ mirror of the checker behind the same
  flag.  (2) The defect of (1) above: the fix in N, an issue, and the
  values of oneCaptured as a test of both backends.  (3) Not checked: the
  lifts of the constraints of =:= and =:<= (FS-beta, E-lift; the memo's
  6.5 does not list them), the schedule replay of 6.5 (B3, S3: the Python
  backend has one schedule; the harness compares the rotation modes of the
  C++ backend), the private copies of the spine that a set function makes
  for a configuration (evalS and the clone of a capsule), and the
  one-constructor case of X-c (such a variable has no root identifier to
  decide).  (4) Two approximations to state in the paper or to tighten:
  "argument-derived" is read from the tags (the boxes above the creation
  site, the entry flags and their inheritance), not from per-cell flags of
  every cell, and the cells a step creates are flagged by a walk from the
  redex that stops at flagged cells; the reduct comparison of X-b' is a
  digest within a budget, so a goal whose configurations exceed 5000 cells
  per instantiation is counted, not compared (none of the suite's goals
  did).  (5) tests/manifest.json has no entry for unit_py_checker.py (a
  calibration run adds it).  (6) The deferred nested-guard assertion of
  the Python backend (capNested, nestFail, nestedBound) stays as it was;
  those goals are left out of the known-good set.  (7) The report of a
  violation under sprite-exec: either print_error prints an
  InvariantViolation in full (a change to the output of sprite-exec), or
  the py-check system of the harness sets SPRITE_DEBUG=1 in its
  environment (the harness side); and HARNESS_AS_LIMIT may need to rise
  for py-check (the memory the checker holds).  (8) A hand-written tree
  table for the strict built-ins (plusInt, eqInt, ltEqInt, eqChar, cond,
  &) would put their demand order under B2 as well.  (9) Nothing is
  committed from this lane.

2026-10-09: three findings of the differential harness (#119, #120, #121;
  worktree wt/bugs3)
- The programs.  The harness of the Fair Scheme proofs generated the three
  programs on 2026-10-09 and the owner filed them.  Each one is a test
  module now: data/curry/SharedListPrinter.curry (#119, pinned by
  unit_show.py), data/curry/PyGeneratorAssert.curry (#120) and
  data/curry/PyFingerprintNone.curry (#121, the module Prog of the harness
  under the name of the file), both pinned by TestHarnessFindings of
  unit_setfunctions_bugs.py on both backends.
- #119, the C++ printer wrote "..." for a shared list without a cycle.
  The cause: StrStringifier (graph/show.cpp) kept every node met in a
  multiset and took a second count for a cycle.  The exit callback of the
  walk removed walk->cursor(), which is the last successor visited at the
  level the walk leaves, not the node left, so a node stayed in the
  multiset after the walk left it, and its next occurrence printed as an
  ellipsis: ([False], ...) for let d = [False] in (d, d), [[1], ...] for
  let d = [1] in d : d.  The fix: the printer keeps the path from the root
  to the current node (a vector, with a multiset for the lookup).  A node
  enters the path when the printer extends the walk below it (the new
  member extend, which every extension of StrStringifier goes through) and
  leaves it in the callback (the last callback, for the root level of the
  walk, closes no extension).  A node met again while it is on the path is
  a cycle; a node met again elsewhere is shared and prints in full.
  is_terminus went: a node the walk never extends is never on the path.
  ReprStringifier was right already (its callback data is the node).  The
  Python printer (show.py) had a leak of its own: format_as_list counted a
  spine node that closed a cycle and never took the count back, so a
  cyclic list shared by two positions printed as ([1, ...], ...) for
  let a = 1:a in (a, a).  Fixed: the failed push is popped.  Both backends
  print ([1, ...], [1, ...]) now, and every cyclic text observed before the
  change stays: [1, ...], [1, 2, ...], Just ..., (..., 1), [...], ... .
  The review found a second limit of the Python printer: the string style
  (format_as_string) walked the spine of a list of Chars with no check for
  a cycle, so str of let s = 'a':s in s never ended on the Python backend,
  where the C++ printer writes ['a', ...].  Fixed: the walk keeps the nodes
  it met and gives up at the first repeat; the list style then writes the
  ellipsis.  Both backends print ['a', ...] and (['a', ...], ['a', ...]).
  Tests: unit_show.py (14 tests, both backends): the two goals of the
  issue evaluated, seven shared shapes (tuple, constructor, string, list in
  list, open list, head and tail), and the cyclic shapes with their texts,
  the shared cyclic list and the cyclic string among them.  test_circular
  of unit_expr.py holds.
- #120, the Python backend asserted in get_generator when sortValues
  compared values with free variables.  The cause: the graph copier that
  makes a value (rts_control.make_value, also the values of a set function
  inside allValues) copied the node of a free variable, so the value held
  a second node with the id of the variable.  The comparison narrowed the
  copy: hnf made the generator on the copy (instantiate rewrites the node
  at hand), and the next hnf of the same copy found the generator on it
  but read the node of the id from the table (get_generator goes through
  vtable), where the original had none; the assert followed the vain
  constrain_equal of the original with itself.  The C++ copier shares a
  free variable ("one node stands for one id", graph/copy.cpp), so the C++
  backend never had two nodes.  The fix: the Python copier shares a free
  variable too and counts it as before (external_freevars).  The comments
  of make_value, InterpreterState and GraphCopier say so.  The expected
  values: the issue records four values of the C++ backend at 1b7ae501.
  The C++ backend at 7eeac2e9 (this base) prints six, the product of the
  three values of f3 (Just True) (S (S Z) twice, Z) and the two sides of
  x15, the free variable of the argument that the capsule returns among
  its values and that sortValues narrows outside; the runs.jsonl of
  campaign 1 records the same six for KiCS2 and for every C++ mode.  The
  four were a loss of the shared capsule before #86 (two of the three
  alternatives of the first component lost x15 = True).  The test pins the
  six on both backends.  Tests: test_free_variable_in_values
  (unit_setfunctions_bugs.py, both backends) and
  test_free_variable_is_shared (unit_py_copy.py).
- #121, the Python backend raised TypeError in read_fp with the id None.
  The cause: the step of =:= (constr_eq in currylib/prelude/constr.py)
  built the pair of a strict constraint with rts.expr((lhs, rhs)) from the
  two Variables, that is from their rvalues, which carry the set guards
  crossed on the way to the target.  In the program the right side, the
  variable x19 of the goal, is reached through the box of a captured
  argument (#117), so the pair held SetGuard(sid, x19), and obj_id of the
  guard is None: D passed it to constrain_equal, equate_fp, update_fp and
  read_fp.  The C++ step builds pair(lhs.target, rhs.target), the two
  variables themselves.  The fix: the Python step does the same for the
  strict constraint.  Test: test_constraint_through_a_guard
  (unit_setfunctions_bugs.py, both backends): (_a, _b, []) and
  (_a, _b, [False]) twice, the values of the C++ backend; the record of
  campaign 1 shows KiCS2 with seven values, x19 instantiated, a difference
  of the kind of row 2 of the memo and not the acceptance.  The review
  found the same cause in make_value_bindings (eval/rts_bindings.py): the
  pair of the value binding of a variable of a builtin type took the
  Variable that hnf passes, that is its rvalue with the guards, so for a
  variable of the goal reached through the box of a captured argument the
  pair held SetGuard(sid, y).  constrain_equal read the id None from both
  sides, took the sides for equal and returned True without a binding, and
  the comparison that followed suspended: let y free in
  (y, sortValues (set1 k2 y)) with k2 m = (m =:= 1) &> (m == 1) printed
  "Evaluation Suspended" on the Python backend, where the C++ backend
  prints (_a, [True]) (the binding is private to the capsule).  Fixed: the
  pair holds the node of the variable, as the C++ step does.  Test:
  test_value_binding_through_a_guard (unit_setfunctions_bugs.py, both
  backends) on the goal valueBinding of PyFingerprintNone.curry.
- Verification (the machine shared; the C++ runtime built with make stage
  and make overlay after the change to show.cpp, which is no header of the
  closure, so no object went stale).  The probes of the issues before the
  change: ([False], ...) and ([False], ..., 1); AssertionError; TypeError
  after the first value.  After: ([False], [False]), ([False], [False], 1)
  and ([], [], 1) on both backends; six values on the Python backend, the
  same texts in the same order as the C++ backend; three values.  The
  runner, both backends, -j 2 --mem 4: unit_setfunctions_bindings.py,
  unit_setfunctions_semantics.py, unit_setfunctions_bugs.py,
  unit_py_residuation.py, unit_cxx_freevars.py, unit_show.py,
  unit_py_copy.py, unit_expr.py and unit_py_constraints.py: 18 of 18 run,
  0 failed, 31 s (one run before it had the sorted order of one expected
  list wrong in the new test, corrected).  The fast tier, --fast --backend
  both: 126 of 126 run, 5 failed, 128.7 s; the five (unit_cxx_rewrite.py,
  unit_inspect.py, unit_cxx_bom.py, unit_cxx_partial.py,
  unit_cxx_literals.py on cxx) read the compiled object of a corpus module
  that the fresh worktree had not compiled (KeyError 'cxx.shlib', the
  bytecode in place of the generated code); after --prepare-only --backend
  both -j 2 --mem 4 (24 of 24, 0 failed; the first run compiled the main
  corpus and was cut at 400 s, the second finished the rest in 88.6 s) the
  five pass: 5 of 5, 0 failed, and the fast tier again: 126 of 126 run,
  0 failed, 108.2 s.  Not run here: the full unit suite, the oracle suites
  and the documentation build (the suite stage).
- Left to the owner.  (1) The text of #120 names four values; the test
  pins six, with the reason above; the comment on the issue is the
  owner's.  (2) The pair of a non-strict constraint (nonstrict_eq) still
  comes from the rvalues: a free variable on the left of =:<= reached
  through a guard would give add_binding the id None the same way; no
  program showed it, and the C++ step drops the guards of both sides
  there, where the Python step keeps the guard on the bound expression (a
  parity question of the box rule, not changed here).  The strict
  constraint and the value binding take the nodes now; the box rule for
  the non-strict pair is the owner's decision.  constrain_equal itself
  still returns True for two arguments without an id (obj_id gives None
  for a node that is neither a choice nor a free variable); a check there
  would turn the next instance of the cause into an error instead of a
  dropped binding, and is left to the same decision.  (3) curry.raw_expr
  with a cyclic anchor referenced twice loops in fix_references until
  MemoryError: raw_expr([anchor(cons(1, ref()), name='a'), ref('a')]) and
  raw_expr((ref('a'), ref('b')), a=cons(1, ref('b')), b=cons(2, ref('a')));
  the tests of unit_show.py build the shared cyclic shapes from a node
  instead.  (4) The two printers write a partial application of an
  operator differently, ((:) ...) against : ..., observed on
  let a = (:) a in a; not pinned.  (5) The printed text of a cyclic list
  shared by two positions changed on both backends, from ([1, ...], ...)
  to ([1, ...], [1, ...]), the rule being an ellipsis only where the walk
  meets a node of its own path.  The acceptance of #119 ("a shared cyclic
  structure still prints with ...") can be read either way: the closing
  comment of the issue should state the rule, or, if the old text is
  wanted, both printers need a memo of the cyclic nodes already written
  and test_shared_cyclic_list changes.  (6) Nothing is committed from this
  lane.

2026-10-09: the C++ mirror of the checker of memo 6.5 (the run-time
  invariants of the Fair Scheme behind the flag checker; worktree
  wt/cxxcheck)
- The task.  Section 6.5 of the memo on the Fair Scheme proofs asks for the
  checker mode first in the Python backend, then mirrored in the C++ runtime
  behind a flag.  The Python checker landed on 2026-10-09 (commit 80dc8043,
  merge ccd10d2c).  This entry delivers the C++ mirror: the frame, the
  checks of parts (2) to (5) of the lane, the tests, the cost with the flag
  off, the violations found, the differences from the Python checker, and
  what is left to the owner.
- The flag.  The same flag ``checker`` (True | False, default False) of
  curry.interpreter.flags, read by the C++ runtime too.  The exact text for
  a run: SPRITE_INTERPRETER_FLAGS=backend:cxx,checker:true.  For the systems
  table of run.py the system cxx-check is the pair
  ('backend:cxx,checker:true,interpret:all', 'steps:65536'): under the
  tiered default after the prepare pass of the harness B2 is checked for
  0.6% of the hnf calls (411 of 66545 on the 165 known-good goals of the
  test file, one runtime state per goal), under interpret:all for 33.4%
  (22254 of 66538; the rest are the built-ins of currylib, which have no
  tree on either backend); the tiered pair ('backend:cxx,checker:true',
  'steps:65536') is a second system when the harness wants the compiled
  steps exercised under the other invariants.  The bindings pass the flag as
  the last argument of RuntimeStateBase (cyrtbindings/__init__.py reads
  interp.flags).
- The design.  RuntimeState::checker (src/cyrt/state/rts.hpp) is a pointer
  to one Checker of src/cyrt/checker.hpp while the flag is on and null
  otherwise; the state owns it (rts.cpp).  Every hook in the runtime is one
  test of that pointer.  The header checker.hpp is outside the include
  closure of cyrt/cyrt.hpp: the generated code never sees it.  The closure
  changed in two headers, fwd.hpp (the forward declaration) and rts.hpp (the
  pointer, the constructor argument, and the two pull_tab members, which
  were static and now reach the checker through this), so the ABI stamp
  changed once and every compiled product was stale (the prepare pass ran,
  see the runs).  The pointer is last among the plain fields of the state,
  so the offsets generated code may read are unchanged.  The checker
  observes: it reads the union-find without path compression, the
  fingerprints without the growth of check_alloc (it enumerates the used
  bits of the blocks of the tree), and the bindings without the absorption
  of get_binding.  It keeps alive, as roots of the collector (gc_add_root),
  every cell it flags and one box of every capsule it enters, so the
  addresses in its tables stay those of the objects they name (the table of
  the generators compares its entries by address alone and roots nothing: a
  variable alive keeps its generator, and the id of a dead one never
  returns); under the MPS collector it holds a GcClamp for the life of the
  state.  A violation throws InvariantViolation (std::logic_error) from the
  hook, through the scheduler, and reaches Python as
  curry.backends.cxx.cyrtbindings.InvariantViolation, an AssertionError
  registered by the bindings (exceptions.cpp), as the step limit reaches
  Python through StepLimitReached; its message names the invariant, the
  event, the detail, the identifiers, the configuration (its queue and set,
  its index, escape_all, the fingerprint, the groups, the goal position from
  the scan, the root expression) and, for a dispatch-chain disagreement, the
  enclosing configuration.
- The hooks (the events of the Python checker).  fork: rts_fingerprint.cpp,
  after the clones went into the queue and before the parent leaves it (the
  two clone pointers are kept in a stack array as the lambda pushes them).
  escape: allValues_step of currylib/setfunctions.cpp, around Queue::split,
  with the configurations of the queue before the split.  pull-tab:
  RuntimeState::pull_tab (the core form), before the copies of the spine are
  made and after the choice over them; the path comes from the scan of the
  configuration (the levels from the slot of the source down to the target),
  which the Variable form pushed.  yield: release_value, before the value is
  made.  freshvar, _make_generator and clone_generator (rts_freevars.cpp).
  The write of a generator into the slot of a variable, rule S.x:
  instantiate, and the slot write of an existing generator in
  replace_freevar (the has_generator branch), before and after the write.
  The private copies of rule N.x in replace_freevar (the binding, the
  generator and the representative of the group) and in the binding case of
  the inductive form, after the copy is made and before it replaces the node
  in the slot, which still holds the old spine.  The value bindings of a
  built-in type.  step: procS, before the step function runs (the symbol and
  the successors of the redex are recorded on a stack, since steps nest) and
  after it returned; a status below E_RESTART is not a completed step.  hnf:
  at its entry.  Capsule entry: evalS_step, after the queue of the capsule
  is made.
- The checks.  (2) B1 as in the Python checker: at every fork, escape and
  yield the fingerprint of each configuration involved is a function (a used
  bit is LEFT or RIGHT, so the undetermined entry of the Python check cannot
  occur), its group invariant holds, and the fingerprints along the dispatch
  chain agree (the enclosing fronts of the queue stack; at an escape the
  chain includes the configuration that runs allValues, since the queue of
  the capsule is off the stack there, as in the Python backend); each clone
  of a fork keeps every decision of its parent, has the decision of the
  fork, in its own fingerprint or in an enclosing one, and adds at most the
  forked identifier and its group root (rule D.2; a fork on a free variable
  may add more); at most two clones with different sides.  The cost of these
  checks is flat along a chain of narrowings where the Python checker's is
  linear in the entries of the fingerprint: the group invariant is read over
  the ids the union-find united (every member of a group of two or more is
  among them, and the invariant can fail at a member alone; the check rests
  on unite as the only writer of the union-find, which appends both ids to
  the list, and a writer that bypassed it would silence the check:
  test_group_invariant_at_fork reads the list through checker_debug_united),
  the dispatch chain is read only inside a capsule (the entries of the
  nested fingerprint), and rule D.2 is read from a structural diff of the
  clone's fingerprint tree against its parent's, which skips the blocks the
  two share (copy on write) and falls back to every entry when the clone
  grew its tree.  The first form enumerated every entry at every fork:
  narrowEach 40 x 5000 and the million narrowings of unit_cxx_freevars.py
  ran past the 300 s of their children (quadratic), and run in 6.5 s and
  11.4 s now.  A pull-tab: the source is at the recorded path, the two
  alternatives are fresh copies of the spine that share the cells off the
  path (the raw words of the slots compared) and end in the alternative, the
  created choice carries the identifier of the source, and the identifier is
  in the escape set of every box the path crosses (def:s-pull).  At a yield
  no choice, operation, failure or constraint cell is reachable from the
  value; the generator of a free variable and the arguments of a partial
  application are not entered.  (3) FS-x.  X1: one generator per variable,
  its root a choice with the variable's identifier, at the creation and at
  every write.  X-c at a yield: no bound or decided variable remains in the
  value, and for every variable of the table with a generator and a decided
  root the identifiers on the decided path of its generator are decided.
  X-b at a write of a generator into a slot (the memo's Inst-slot: the C++
  runtime writes the slot in place where the Python backend copies the
  spine): the slot held the variable, holds the generator after, and the
  reduct of every configuration that references the redex, read under the
  fingerprint it is compatible with, is the same before and after (a lone
  configuration, one queue of one configuration with no capsule entered, is
  the only one that references the redex and reads the variable and the
  generator alike, so the comparison is skipped there and counted as
  reducts_skipped: a sequence of a million narrowings stays linear); X-b'
  after a private copy of rule N.x: the copy is faithful and its reduct
  equals the reduct of the root it replaces; a copy whose slot is not a
  level of the scan is reported as a violation of X-b' (copy_spine copies
  the levels of the scan from the slot down, so such a slot is a defect of
  the copy, not a case the check can skip; the first form counted it as
  copy_untracked, and no program of the suite reaches it).  The reduct is
  read with the generator image of the memo's revision (a decided choice is
  its side, a bound variable its binding or its generator, a free variable
  and an undecided generator root the group of the variable, a forward node
  or a box passed through) and compared as a 64-bit digest (a splitmix mix
  over the cells, the tables by address) within the budget of 5000 cells per
  event; an event over the budget is counted (over_budget), not compared.
  (4) FS-S as in the Python checker: every identifier is tagged at its
  creation with the boxes above it (the guard levels of the scans of the
  dispatch chain, the pushed inductive paths included, and escape_all) plus
  the flags of the redex cell; the flags are the entry walk of a capsule
  (bounded by 50000 cells; a capsule beyond the budget turns its S0 checks
  off) and their inheritance by the cells a pull-tab, a copy or a generator
  makes at a flagged cell (bounded by 1024).  S0: an identifier that enters
  an escape set is argument-derived for the set (E in A; seen at the
  pull-tab across the box, where the runtime inserts, and recorded in the
  shadow of the set); a fork inside a capsule is on an identifier that is
  function-derived for the capsule, in the escape set of no enclosing
  capsule and decided by no enclosing configuration, or on one the queue was
  split on; at SF.1 the escaping identifier is argument-derived for the
  capsule or an enclosing one, or an enclosing configuration decided it.
  The escape sets only grow (a shadow per set with a box; a loss and an
  insertion outside the rule are both reported).  S-split: after SF.1 the
  kept queue holds the LEFT and the undecided configurations, the new one
  the RIGHT and the undecided, nothing else and nothing lost, both record
  the identifier, both name one set.  (5) B2 for the interpreted steps: the
  definitional tree of an operation is rebuilt from its bytecode
  (cyrt/icurry.hpp) at the first use and kept, with the logical positions of
  the case variables (BIND_ROOT, BIND_VAR, COPY_VAR; a STORE_VAR is not a
  position), one subtree per constructor tag or literal value, and three
  kinds of leaf (exempt, a return of a built node, a return of a reference).
  At every hnf of such an operation the logical path of the inductive
  position (the real path less the steps through forward nodes and guards)
  is a case of the tree whose branches above it match the constructors at
  their positions (B2 a, b).  After a completed step whose redex became a
  failure the matched path ends in an exempt leaf, in a literal case without
  a branch for the value, at a failure at an inductive position, or in a
  return of a reference (B2 c; see the differences).  A compiled step
  function has no tree: under interpret:off, and under tiered for a module
  loaded from its object, its hnf calls are counted (hnf_untracked), not
  checked, as the built-in steps of currylib are on both backends.  A
  compiled check would need the tree from another source: the bytecode kept
  beside the compiled step (Module::adopt clears aux at the swap today), the
  ICurry of the module handed down from the Python side when the flag is on,
  or a tree table the emitter writes into the object.  Under the tiered mode
  an operation stepped before the swap keeps its tree after it.
- The differences from the Python checker (also in the developer notes).
  (a) A replacement by failure has three sources here: an exempt leaf, a
  failure at an inductive position (a completed step in C++: hnf forwards
  the redex to the failure and the step returns, where the Python backend
  unwinds and its check never sees the step), and a return of a reference
  (RET_REF forwards the redex to the node the reference denotes, which may
  be a failure; the Python backend writes a forward node there).  The first
  form of the check reported the second source on every goal with a failing
  argument (nilP, anyOf, &>, _impl#==#Prelude.Eq#[], nubList) and the third
  on none; both are accepted now.  (b) The write of rule S.x is a slot
  write, checked as X-b, and the slot write of an existing generator is
  checked too (unhooked on the Python backend); no flags propagate at the
  write, since no cell is made.  (c) The reduct of the generator choice of a
  bound variable is the binding, as the reduct of the variable is: the C++
  write puts the generator into a slot that a configuration with a binding
  of the variable may share (sibling of SetFunctionsBindings: the left
  alternative binds x, the right alternative narrows the shared length x),
  and the fork of that configuration applies the binding to the generator.
  The first form reported it as X-b; a false alarm, fixed.  (d) The reduct
  of a variable without a generator of its own, decided through its group,
  is the generator of the representative: the node the runtime puts into the
  copy of rule N.x (get_generator reads it by the group id), where the
  Python backend clones a generator for the variable first.  The first form
  reported the paired goal (x =:= y, pick x =:= 2) as X-b'; a false alarm,
  fixed.  (e) The configurations whose reducts are compared are those of the
  queues of the dispatch chain and of the capsules their graphs reach (the
  SetEval nodes the walk meets), not every queue of the collector's
  registry: the registry holds the queues of other evaluations and of
  unloaded interpreters, whose nodes this evaluation must not read (the
  first form walked gc_queues() and read the info table of a node of an
  unloaded module after a curry.reload: a segmentation fault in
  test_setfunction_semantics; the accessor is gone).  A capsule reachable
  from a yielded value alone is not read.  (f) The step propagation of the
  flags is a no-op on both backends: the walk starts at the redex, which
  carries the flags already, and stops there, so a cell a step makes at a
  flagged redex inherits nothing (the pull-tabs, the copies and the
  generators propagate).  The consequence, found in the review of the lane
  and observed on both backends: a boxed argument whose evaluation outside
  the box, after the capsule started, creates a choice gets a false report
  of S0 (E in A) at the pull-tab across the box.  The program: g2 n = case n
  of { 1 -> Just (A ? B); _ -> Nothing }, f2 x = 0 ? (case x of Just y ->
  case y of { A -> 1; B -> 2 }), lazyThenOutside2 = let a = g2 (length [()])
  in let vs = valuesOf (set1 f2 a) in (head vs, fromJust' a, vs).  The
  capsule yields 0 without demanding a; the enclosing configuration steps g2
  at the flagged cell (the result Just (A ? B) holds a fresh ? cell, which
  inherits no flag), then ?, whose choice carries the tags of the boxes
  above it and the flags of the ? cell, which are none; the second
  alternative of the capsule pulls the choice across the box, and the
  insertion into the escape set is reported: "S0 (E in A) violated at
  escape-set insertion: identifier 1 enters the escape set of set #1 but is
  not argument-derived for it (tags [])", root (f2_CASE1 {box #1|(?1 A B)}),
  exit 1 under sprite-exec on both backends; without the flag the runtime
  gives (0, A, [0, 1]) and (0, B, [0, 2]), the values of weakly encapsulated
  search.  The same on twoCapsulesChoice2 (two capsules over the argument)
  and capturedThenShared2 (the argument captured by a second set function);
  the inlined form g _ = Just (A ? B) passes because the ? cell exists at
  the entry of the capsule.  The seven known-good files do not hold the
  shape.  TestSharedArgument of unit_cxx_checker.py and unit_py_checker.py
  pins it as an expectedFailure on both backends, with the values without
  the flag and the text of the report.  Not changed: the fix is the owner's,
  item (3) below.  Fixed on 2026-10-09 (the entry "#124 and the checkers'
  propagation" below).  (g) escape_all is never set by the C++ runtime; the
  hook reads it for symmetry.  (h) The private copies of a set function
  (evalS_step, the clone of a capsule) are not checked on either backend.
- The tests (tests/unit_cxx_checker.py, 56 tests on the C++ backend; the
  flag tests and the values of TestSharedArgument run on both).
  TestKnownGood (in the process, under its mode) and
  TestKnownGoodInterpreted (a child per run under interpret:all, set through
  the environment before the first import; see the defect below for why not
  a reload): the goals of the probe table under both failure rules, the
  goals of SetFunctionsBindings, the goals of unit_setfunctions_bugs.py that
  end (pickOverlap does not end, by its issue), the functional-pattern
  modules, the goals of unit_cxx_freevars.py, the module with the narrowed
  name, the released residual, the narrowing and the pairing, and the
  program of the dropped-box defect of the Python backend; each runs under
  the checker without a report and with the values it has without the flag
  (both runs in the test), the steps and the forks of four searches are the
  same with the flag on and off, the counts show the events, and under the
  interpreter the hnf calls checked outnumber the untracked ones.  One
  constructed violation per check, through the debug entry points of the
  bindings (cyrtbindings.checker_debug_*, each refused while the flag is
  off; cyrtbindings/checker.cpp): the group invariant at a fork of the
  scheduler (a united pair with a decided member), a clone that lost a
  decision and one that adds identifiers (the check of rule D.2 on
  constructed fingerprints), the identifier, the copies and the path of a
  pull-tab, a choice and an operation in a yielded value, a nested
  configuration against its enclosing one, a second generator and a wrong
  root identifier, a decided variable in a value, an undecided inner
  identifier, a copy whose reduct changed, a write whose configuration was
  corrupted beside the slot and a write whose slot does not hold the
  generator, a pull-tab across a box of a function-derived identifier (E in
  A) and one without the insertion (def:s-pull), a fork of the scheduler
  inside a capsule on an argument-derived identifier, a fork inside a nested
  capsule on an identifier of the escape set of the enclosing set and one on
  an identifier the enclosing configuration decided (the checks of the fork
  on the front configuration), a split on a function-derived identifier (a
  split of the runtime), a split that keeps a decided configuration in both
  queues, an escape set that lost an element and one that gained one outside
  the rule, the tree of an operation (built from the bytecode when the
  operation is interpreted; None when compiled), a demand of a position no
  matching branch needs, a failure from a return leaf (with the exempt leaf
  and the returned reference accepted), and the report itself (an
  AssertionError with the invariant, the identifiers, the configuration and
  the goal position).  The review of the lane added: the ids of a united
  pair in the list the group check reads (checker_debug_united), a clone
  that does not record the decision and two clones that decide one side
  (checker_debug_check_fork with constructed clones), a bound variable in a
  value (checker_debug_bind), the three other reports of S-split (the queues
  do not both record, the new queue holds a configuration that was not in
  the queue, the new queue names another set; the variants of
  checker_debug_check_escape_shared), a demand under an inductive position
  that holds an operation and under a constructor no branch matches, and a
  failure under each (B2 and B2 (failure), on nodes ill-typed on purpose),
  and TestSharedArgument, the known false alarm of (f), in both test files.
- The cost with the flag off.  The steps and the forks of the goals of
  unit_funcpat_narrowed.py are unchanged, before and after, and the same
  under the checker: one/source 145 steps 8 forks, one/sourceS 138 and 8,
  one/sourceN 162 and 8, many/source 219524 and 1202, many/sourceS 12102 and
  8, many/sourceN 177745 and 1202, distinct/source 9529 and 324,
  distinct/sourceS 1690 and 72, distinct/sourceN 10231 and 450.  The
  benchmarks (run_benchmarks -r 3 -b cxx --no-perf, before and after, the
  machine shared with a test run): compare --deterministic finds the
  counters (steps, forks, collections) equal for QueensSet, QueensSet9 and
  Tak1 (2089740, 11784795 and 33629915 steps); the CPU seconds (the median
  of three) are 0.857 before and 0.799 after for QueensSet (ratio 0.93),
  5.154 and 5.694 for QueensSet9 (1.10), 1.480 and 1.438 for Tak1 (0.97):
  two within the noise threshold of the harness, one at its edge in a shared
  run, and on the quiet machine at the end of the lane (-r 5) 0.752, 4.743
  and 1.360 s after, the ratios 0.88, 0.92 and 0.92: the 1.10 was the load.
  The flag-off path has one test of the pointer per step (two: before and
  after the step function), per hnf, per pull-tab (two), per fork, per
  yield, per copy, per generator, per write, per fresh variable, per escape
  (two) and per capsule entry, and the fork keeps its two clone pointers in
  a stack array with the flag off.  With the flag on the cost of a fork is
  linear in the entries of the fingerprint (B1 reads every entry), so a deep
  search pays: the diverging pickOverlap of SetFunctionsBugs takes 3.7 s for
  its first 30000 steps and 55 s for 100000 (0.00 s and 0.02 s without the
  flag); the Python checker has the same shape.  The checker holds memory as
  well: every flagged cell, every generator and one box per capsule are
  roots for the life of the runtime state.
- The runs (the build in the clean environment: make stage, make overlay,
  then ./run_tests --prepare-only --backend both -j 2 --mem 4 after the
  header change: 24 of 24 run, 0 failed, wall 588 s).  tests/
  unit_cxx_checker.py alone, under the process flags of the runner: tiered
  48 tests ok in 41 s; interpret:off 48 tests ok, 2 skipped (the hnf and the
  failure tests need a tree, and the operation is compiled), 58 s.
  tests/unit_py_checker.py on the C++ backend: 38 tests ok, 35 skipped.  The
  seven files of the lane under the checker through the runner (-j 1 --mem 4
  --backend cxx, SPRITE_INTERPRETER_FLAGS= backend:cxx,checker:true): the
  first run, before the incremental form of the B1 checks, 7 of 7 run, 1
  failed (unit_cxx_freevars.py: the children of
  test_million_narrowings_run_flat and test_table_shrinks_at_a_collection
  past their 300 s; the other six files ok: unit_setfunctions_bugs.py 25
  tests, unit_cxx_scheduler.py 9, unit_setfunctions_semantics.py 41,
  unit_setfunctions_bindings.py 16, unit_funpat_free_argument.py 9,
  unit_funcpat_narrowed.py 2), wall 639 s; under interpret:all as well (...,
  interpret:all): 7 of 7 run, 1 failed (the same two children), wall 781 s;
  after the incremental form unit_cxx_freevars.py runs 7 tests ok in 20.5 s
  compiled and 35.5 s interpreted, and the final runs of the seven files:
  compiled 7 of 7 run, 0 failed, wall 40.0 s (unit_cxx_freevars.py 7 tests
  20.5 s, unit_setfunctions_bugs.py 25 tests 5.0 s, unit_cxx_scheduler.py 9
  tests 2.5 s, unit_setfunctions_semantics.py 41 tests 2.0 s,
  unit_setfunctions_bindings.py 16 tests 5.0 s, unit_funpat_free_argument.py
  9 tests 1.0 s, unit_funcpat_narrowed.py 2 tests 1.0 s), and under
  interpret:all 7 of 7 run, 0 failed, wall 185.1 s (36.0, 50.5, 19.0, 17.0,
  52.5, 4.5 and 2.5 s).  The unit suite on the C++ backend with the flag off
  (./run_tests -j 1 --mem 4 --backend cxx 'unit_*.py', the new file
  included): 97 of 97 run, 1 failed, wall 610.9 s: unit_cxx_rotation.py,
  whose test_zero_quantum_is_an_error builds RuntimeStateBase with the eight
  arguments of before; the bindings keep that constructor (the checker off)
  beside the one with the flag, and a rerun passes unit_cxx_rotation.py (20
  tests, 9.5 s), unit_cxx_checker.py (48 tests, 36.5 s) and
  unit_py_checker.py (38 tests), 3 of 3 run, 0 failed.  make -C docs html:
  build succeeded.After the review of the lane (the stage and the overlay
  built again, exit 0; checker.cpp and the bindings alone changed, so the
  ABI stamp did not): unit_cxx_checker.py 56 tests ok, 1 expected failure,
  in 40.4 s under tiered and 47.9 s under interpret:off (4 skipped), and 56
  tests, 53 skipped, on the Python backend; unit_py_checker.py 41 tests ok,
  3 expected failures, on the Python backend, and 41 tests, 37 skipped, on
  the C++ backend; the seven files under the checker through the runner:
  compiled 7 of 7 run, 0 failed, wall 38.5 s, and under interpret:all 7 of 7
  run, 0 failed, wall 171.6 s (34.5, 45.5, 17.5, 16.0, 48.0, 4.5 and 2.5 s);
  the 165 known-good goals under the checker with the experimental step
  propagation of item (3): no violation, the same totals as without it; make
  -C docs html: build succeeded.
- The violations found.  No invariant violation of the C++ runtime on the
  goals of the lane: every report of the first forms was a false alarm of
  the checker, fixed as listed under the differences ((a) the failure at an
  inductive position, (c) the bound variable's generator, (d) the
  representative's generator), and the segmentation fault of (e) was the
  checker's own.  The dropped-box defect of the Python backend (oneCaptured)
  passes here with the values of weakly encapsulated search, as the Python
  checker's entry expected.  One defect of the runtime outside the checks,
  found by the new test file under interpret:off, with the flag off: a
  process that ran the goals of SetFunctionsBindings compiled
  (curry.reload({'backend': 'cxx', 'interpret': 'off'}), import, every
  nullary goal evaluated) and then reloads into interpret:all
  (curry.reload({'backend': 'cxx', 'interpret': 'all'}), import, the same
  goals) dies with a segmentation fault at the goal sibling, in procS at the
  call of the step function of the redex, whose address lies in an object no
  longer mapped (gdb: #0 0x00007ffff20d3700 in ?? (), #1
  RuntimeState::procS): a node of the new interpreter carries an info table
  whose step points into the compiled object of the module the reload
  unloaded; the goals before sibling run.  The same under tiered once the
  compiled object of the module exists (the prepare pass of the runner makes
  it): the first run of the test file, before the prepare pass, interpreted
  the module under tiered and survived the reload.  A process that starts
  under interpret:all runs the goals twice, with a reload in between,
  without a crash.  Not analysed further and not fixed in this lane; the
  reproducer is the sequence above (the probe reload_probe.py of the lane's
  scratch directory ran it); the interpreted known-good tests of
  unit_cxx_checker.py run in a child per run, with the mode set before the
  first import, because of it.  Verified on the base commit in the review of
  the lane: the install of ee051cbd, whose runtime is that of ccd10d2c (git
  diff --stat between the two prints nothing under src/cyrt and
  backends/cxx), dies the same way (exit 139 at sibling) when the probe runs
  with CURRYPATH at a copy of the test-data directory after the prepare
  pass, under PROBE_FIRST=off and under tiered.  The trigger is the compiled
  object on disk from an earlier process: with CURRYPATH at a directory that
  holds SetFunctionsBindings.curry alone, under SPRITE_CACHE_FILE= and
  SPRITE_PRODUCT_CACHE=, the first run of the probe (interpret:off compiles
  the module in the process) runs the 35 goals twice, with the reload in
  between, and exits 0; the second run of the same command, in the same
  directory, loads the module from the .so the first run wrote under .curry/
  and dies at sibling after the reload (exit 139); the base install and the
  lane's install behave alike.  The issue should name the object loaded from
  disk as the trigger (the runner's prepare pass writes such objects for the
  test data) and start from the gdb frame above.  Filed as #124 and fixed
  on 2026-10-09 (the entry "#124 and the checkers' propagation" below): the
  trigger is the compiled Prelude, not the object of the module.
- Left to the owner.  (1) B2 for the compiled step functions (the three
  routes above); until then the system cxx-check is the interpret:all pair
  above (B2 for every operation with an ICurry body: 33.4% of the hnf calls
  against 0.6% under tiered), and the tiered pair is a second system for the
  compiled steps.  (2) The two readings of the differences (a) and (c) in
  the paper: a failure at an inductive position as a source of the
  replacement by failure, and the generator choice of a bound variable as
  the binding.  (3) The fix of (f) on both backends: start the step
  propagation of the flags at the successors of the result cell, under the
  budget of the walk (the C++ form: Checker::step_end propagates from the
  result, when it is not the redex, and from each successor of the result;
  the Python form: Checker.step calls _propagate over the successors of the
  node).  Tried on the C++ checker in the review: no report on
  lazyThenOutside2, twoCapsulesChoice2 and capturedThenShared2 (the values
  of the flag-off run), the 165 known-good goals without a report and with
  the same counts; the expectedFailure tests of TestSharedArgument become
  passes then, and the reading of "argument-derived" in the paper follows
  the memo's "inherited by created cells".  Done on 2026-10-09 (the entry
  "#124 and the checkers' propagation" below).  (4) The private copies of a set
  function, the lifts of the constraints of =:= and =:<= (FS-beta) and the
  schedule replay stay unchecked, as on the Python backend; the harness
  compares the rotation modes.  (5) tests/manifest.json has no entry for
  unit_cxx_checker.py (a calibration run adds it).  (6) The report under
  sprite-exec needs SPRITE_DEBUG=1, as for the Python checker; the harness
  side (py-check, cxx-check) can set it.  (7) The pointer in RuntimeState
  and the non-static pull_tab changed the ABI stamp once; no layout the
  generated code reads changed.  (8) Nothing is committed from this lane.

2026-10-09: the dropped box of N and the cyclic anchors of raw_expr (#123,
  #122; worktree wt/bugs4)
- #123, the Python backend encapsulated a captured choice that the walk of
  N reaches through a forward node in front of its box: oneCaptured of the
  issue printed (3, A) and (3, B), where the C++ backend prints (2, A) and
  (2, B).  The checker mode found it (B1 at the pull-tab: the source is
  not at the path of the target).  The cause: constT x 0 rewrites the
  cursor of the walk to a forward node to the guarded x (IReturn of a
  reference emits _0.rewrite(rts.Fwd, ...)), and the T_FWD case of N
  (eval/fairscheme.py) replaced the cursor through logical_subexpr(parent,
  index, update_fwd_nodes=True), which skips the chain of forward nodes
  and the set guards behind it in one go, while the real path and the set
  ids of the walk (state.realpath, state.data) were not extended.  So the
  choice behind the guard was pull-tabbed along a path that ended at the
  guard: copy_spine made the alternatives without the box, and
  update_escape_sets saw no sid, so the choice never entered the escape
  set of the capsule and was forked inside it.  The fix: the T_FWD case
  compresses the chain of forward nodes alone (compress_fwd_chain), writes
  its end into the slot of the parent, as the C++ walk does
  (compress_fwd_chain in procN), and goes on at the end; a guard there is
  met on the next pass and crossed by the T_SETGRD case, which pushes it
  with its sid, so the path and the sids include the box.  The values,
  the set functions and the fingerprints are as before.  Tests: the
  module tests/data/curry/CapturedFwd.curry holds the five goals of the
  issue (oneCaptured, oneCapturedSet, lengthFirst, setFirst,
  capturedThenGuarded); TestCapturedForward of unit_setfunctions_bugs.py
  pins them to the values of the C++ backend on both backends, in
  children; test_captured_forward of unit_py_checker.py runs the module
  under the checker and without it; the expectedFailure marks of
  TestDroppedBox are gone (test_values on both backends,
  test_under_the_checker asserts the values), and test_report builds the
  report of the defect from a pull-tab whose path is one step short of
  its source; capturedThenGuarded joins BUGS_GOALS, which runs under the
  checker.  Before the fix the new tests fail: (3, A), (3, B), and the
  violation under the checker.  Review (2026-10-09): no goal of the suite
  puts a chain of two or more forward nodes, or a guard directly before a
  chain, in front of the walk of N, because the generated code reads an
  argument through a logical path, which compresses the chain before the
  step.  The old walk had a second failure mode there: with a guard met
  directly before the chain it returned a value with the choice still
  inside.  TestForwardChain of unit_py_checker.py pins both shapes and
  the alternating one on hand-built graphs: the sids recorded at the
  pull-tab and the root after it, which keeps every box.
- #122, curry.raw_expr did not return when a cyclic anchor was referenced
  twice.  The cause: fix_references (expressions.py) walks the built graph
  with walkexpr.walk, which has no memo, replaces a placeholder in the
  slot of its parent and pushes every other node.  The replacement closes
  the cycle through the anchor (a = 1 : a), and the second reference to
  the anchor, resolved to the node of the anchor when it was built, led
  the walk into the cycle again.  One reference ends because the
  successors of the node were enumerated before the replacement.  The
  fix: the walk enters a node once (a set of the ids entered); a
  placeholder is still replaced at every slot that holds it.  The typed
  builder (typecheck/builder.py) calls the same function, so curry.expr
  is fixed too.  Test: test_shared_cycle of unit_expr.py, both builders,
  both backends, under cytest.timeout(120): [a, a] with a = 1 : a prints
  [[1, ...], [1, ...]], and (a, b) with a = 1 : b, b = 2 : a prints
  ([1, 2, ...], [2, 1, ...]), each reference the same node.  Before the
  fix the test ends at the timeout.  Review (2026-10-09): the walk did
  not enter an anchor it put into a slot, so an anchor reached only
  through a replaced slot kept its own placeholders: a = 1 : b, b = 2 : c,
  c = 3 : a gave 1:2:failed (a defect older than the memo).  The walk now
  goes on at the anchor after a replacement, and the memo ends it.  An
  anchor whose value is a reference is an alias; the untyped builder
  follows the chain of aliases (_anchor_node), and a chain that returns
  to itself is the trivial cycle a=a.  The typed builder rejects a
  reference to an alias as an undefined anchor, as before.
  test_shared_cycle pins the chain of three anchors on both builders and
  the alias chain on the untyped one.
- Verification (the machine shared; no C++ change, no build).  Probes with
  sprite-exec on copies of the reproducers, before the change: oneCaptured
  (3, A) (3, B) and oneCapturedSet ([[A], [A], [B]], A) ([[A], [A], [B]], B)
  on the Python backend; the C++ backend (2, A) (2, B) and ([[A], [A]], A)
  ([[A], [B]], B); lengthFirst, setFirst and capturedThenGuarded agree on
  both; the reproducer of #122 ran into timeout 60 on the Python backend
  and timeout 30 on the C++ backend (exit 124).  After: the five goals
  print the values of the C++ backend on the Python backend, with and
  without checker:true (SPRITE_DEBUG=1); both shapes of #122 build on both
  backends with both builders.  Single files: unit_expr.py 33 tests on py
  and cxx; unit_setfunctions_bugs.py 27 tests on py, on cxx and on py with
  checker:true (test_captured_then_guarded passes; before the fix it
  failed there with the B1 report); unit_py_checker.py 39 tests on py, on
  py with checker:true, and on cxx (36 skipped).  The runner, both
  backends, -j 2 --mem 4: unit_setfunctions_bugs.py,
  unit_setfunctions_bindings.py, unit_setfunctions_semantics.py,
  unit_py_checker.py, unit_expr.py, unit_show.py: 12 of 12 run, 0 failed,
  30.5 s.  The fast tier on the Python backend, -j 2 --mem 4: 74 of 74
  run, 0 failed, 86.6 s.  Not run here: the full unit suite, the oracle
  suites, the documentation build.
- Left to the owner.  (1) The walk of N splices a chain of forward nodes
  out of its parent, as before and as the C++ walk does; a walk that keeps
  the forward nodes in the real path would copy them into the
  alternatives of a pull-tab, so that form was not chosen.  (2) hnf
  crosses a forward node with Variable.extend, one node per pass, and
  meets a guard behind it on the next pass; it had no share in the
  defect.  (3) The goals longPlainList, viaCall, viaData, viaForward and
  viaPartial of SetFunctionsBugs.curry run clean under the checker
  (probed) and are not in BUGS_GOALS of unit_py_checker.py; adding them
  costs time in that file, the owner's call.  (4) The C++ backend has no
  checker; its walk gave the right values here.  (5) Nothing is committed
  from this lane.

2026-10-09: #124 and the checkers' propagation (the reload from a compiled
  module into interpret:all, and the step propagation of the box flags of
  both checkers; worktree wt/bugs5)
- The task.  Two items left by the checker lanes of 2026-10-09: issue #124,
  a segmentation fault in procS after a reload into interpret:all of a
  process that had loaded the Prelude from its compiled object, and the
  false alarm of S0 (E in A) of both checkers on a boxed argument whose
  evaluation outside the box creates a choice (item (3) of the entry of the
  C++ mirror, difference (f)).  Nothing is committed from this lane.
- #124, the reproduction.  SetFunctionsBindings.curry alone in a scratch
  directory, the probe of the issue, SPRITE_ROTATION=steps:65536,
  SPRITE_CACHE_FILE= and SPRITE_PRODUCT_CACHE= empty: the first run (the
  pass interpret:off compiles the module in the process) prints "off 35
  goals ok", "all 35 goals ok", exit 0; the second run, which loads the
  module from the .so the first wrote, prints "off 35 goals ok" and dies
  with exit 139.  The probe narrowed: the crash needs no goal of the first
  pass (the import alone), and not the module's object: a first pass that
  imports the Prelude alone (curry.getInterpreter().prelude), then the
  reload into interpret:all, then the goal sibling, dies the same way; a
  first pass that imports nothing runs the goal.  Under gdb (the real
  python of the conda environment, PYTHONPATH and LD_LIBRARY_PATH of the
  wrapper): #0 0x00007ffff22d3700 in ?? (), #1 RuntimeState::procS; the
  shared libraries at the crash hold no compiled Curry module, and the
  Prelude's object was mapped at 0x00007ffff2294040-0x00007ffff236b420
  before its dlclose.  At the call, rax held the info table of the redex:
  CyI7Prelude3seq, the static table seq_Info of libcyrt itself (the &> of
  the runtime, cyrt/currylib/prelude/constr.cpp), whose name, format and
  step fields pointed into that range of the old Prelude.so.
- #124, the cause.  prelude.hpp gives the runtime's table of &> the
  exported symbol CyI7Prelude3seq (#define seq_Info CyI7Prelude3seq), the
  mangled name of the Curry function Prelude.seq.  seq and &> are not
  built-ins of the Prelude specification (75 functions; cond and
  nonstrictEq are), so the compiled Prelude defines InfoTable const
  CyI7Prelude3seq{...} for its seq (seq x y = const y $! x) as well.  The
  dynamic initializer of an object constructs a table at the address its
  symbol resolves to, which the dynamic linker binds to the first
  definition in the global scope: libcyrt's, loaded by the bindings.  So a
  load of Prelude.so writes the table of Prelude.seq over libcyrt's seq_Info
  (the mechanism the shim of tiered execution relies on, applied to a
  symbol two libraries define), and the unload of the object at a reload
  leaves the step of the runtime's table in unmapped memory.
  RuntimeState::apply_binding makes a node with &seq_Info when a
  configuration with a binding of a variable forks on its generator; of the
  35 goals sibling is the first that does, so the goals before it ran.
  The nm check over the installed library: of the 130 CyI/CyD symbols
  libcyrt exports, CyI7Prelude3seq is the only one a compiled module of
  the library defines too (Control.SetFunctions imports Values, PartialS
  and SetEval from libcyrt and defines none).  While the object was
  mapped the overwrite was invisible in the values: the node ran the
  compiled seq, const root $! (generator =:<= binding), so a goal that
  applies a binding at a fork took four steps more under the compiled
  Prelude than under interpret:all (sibling and ReloadBinding.bound 35
  against 31, bound3 37 against 33; sprite-exec --stats, steps:65536).
  The first run of the issue survived because the compile on first use
  kept the earlier interpreter alive across the reload, and the object
  with it; a module held across the reload (M at module level) does the
  same (see the test).
- #124, the fix.  The exported name of the table: #define seq_Info
  CyI7Prelude12BindingGuard in prelude.hpp, with the comment, as Choice,
  Fwd and ValueBinding carry names of no Curry symbol.  The identifier
  seq_Info and the step seq_step stay (the table is named "&>"; the
  identifier is historical).  prelude.hpp and checker.hpp are outside the
  include closure of cyrt/cyrt.hpp, so the ABI stamp did not change: make
  stage rebuilt libcyrt and the bindings alone (the objects of the library
  kept their time stamps), and no prepare pass was needed.  After the
  fix: the probe's second run prints "off 35 goals ok", "all 35 goals ok",
  exit 0, the Prelude-only variant runs sibling, and the compiled Prelude
  and interpret:all take the same steps (31, 33, 31), the counts the
  Python backend's &> gives.  The reload from tiered into interpret:all
  once the object exists is covered: the trigger is the Prelude loaded
  from its object, which tiered does as interpret:off does; the child of
  the test from tiered died with status -11 before the fix and passes
  after.  A table of the runtime that is not a built-in but carries a
  Curry name is the whole class; the test below guards the installed
  library against it.
- #124, the tests (tests/data/curry/ReloadBinding.curry; unit_cxx_runtime.py).
  ReloadBinding: len0, bound (the shape of sibling, values 0 1 2 7) and
  bound3 (0 1 3 9).  TestReloadBindingValues pins the values on both
  backends.  TestReloadAfterCompiledObject (C++) copies the module to a
  temporary directory, compiles it with sprite-make --so under
  interpret:off, and runs the sequence in a child: the goals under the
  first mode, a reload into interpret:all, the goals again, as JSON, from
  interpret:off and from tiered.  The child imports the module inside a
  function and collects after the reload, and it reports the compiled
  modules of the library mapped after each reload (/proc/self/maps): the
  test asserts that none is mapped when the second pass starts, since a
  first form of the child, with M at module level, kept the objects mapped
  and passed on the unfixed runtime.  Before the fix both children ended
  with status -11; after, the values of both passes are the pinned ones.
  TestLibraryTablesInTheirObjects (C++): for the Prelude and
  Control.SetFunctions loaded from their objects, the table of every symbol
  lies in the object or in libcyrt (dladdr through ctypes), and a table in
  libcyrt is a symbol the object does not define (the address the object
  resolves for the symbol equals libcyrt's: dlsym through ctypes.CDLL).
  On the old name it fails with "the object of Prelude and the runtime
  library both define CyI7Prelude3seq, the table of Prelude.seq"
  (verified by a revert of the define, a rebuild, the run, and the restore;
  a rebuild of libcyrt takes 6 to 10 s with ccache).
- The checkers, the fix.  Checker::step_end propagates the flags of the
  redex from the result cell, when the step forwarded the redex to a fresh
  cell, and from each successor of the result, through a new overload of
  propagate over several seeds under the one budget of a walk
  (CHECKER_PROPAGATE_BUDGET, 1024 cells per event, unchanged; the entry
  walk keeps its 50000).  Checker.step calls _propagate_all over the
  successors of the node, the seeded form of _propagate with the same
  budget; the single-cell callers (the pull-tab, the copies, the
  generators) are unchanged.  The old walk started at the redex, which
  carries the flags, and stopped there.  Two changes of the walk after
  the review of the lane (the bullet "The review" below): a walk over its
  budget inserts the sets of the flags into unbounded_sets, as the entry
  walk does, and keeps the counter propagate_over_budget; and a leaf
  value (a constructor or Fail without a successor cell: the static True,
  False, Nil, Fail, the small literals and the nullary constructors) is
  neither seeded nor flagged (is_leaf_value in checker.cpp, _is_leaf_value
  in checker.py).
- The checkers, the tests.  TestSharedArgument of both files: the
  expectedFailure mark is gone, test_under_the_checker asserts the values
  under the checker ((0, A, [0, 1]) and (0, B, [0, 2])), and test_report,
  which pinned the text of the false report, is removed (the constructed
  violations of E in A in both files keep the message under test).  Before
  the C++ build the C++ test failed with the report (root (f2_CASE1 {box
  #1|(?1 A B)})); the Python test passed as soon as checker.py changed.
  unit_cxx_checker.py has 55 tests on the C++ backend now, unit_py_checker.py
  44 on the Python backend, no expected failure in either.  The developer
  notes: the step bullet names the budgets and the start of the walk, and
  the paragraph on the shared defect states the fix.
- The runs (the clean environment, CCACHE_DIR set, -j 2 --mem 4).  The
  seven files under the checker: Python backend
  (SPRITE_INTERPRETER_FLAGS=backend:py,checker:true: unit_funcpat_narrowed,
  unit_setfunctions_semantics, unit_cxx_freevars, unit_setfunctions_bindings,
  unit_py_residuation, unit_funpat_free_argument, unit_setfunctions_bugs) 7
  of 7 run, 0 failed, wall 37.0 s; C++ compiled (backend:cxx,checker:true:
  unit_cxx_freevars, unit_setfunctions_bugs, unit_cxx_scheduler,
  unit_setfunctions_semantics, unit_setfunctions_bindings,
  unit_funpat_free_argument, unit_funcpat_narrowed) 7 of 7, 0 failed, wall
  24.5 s; C++ under interpret:all 7 of 7, 0 failed, wall 90.0 s
  (unit_cxx_freevars 33.5 s, unit_setfunctions_bugs 51.5 s,
  unit_cxx_scheduler 18.0 s, unit_setfunctions_bindings 49.5 s,
  unit_setfunctions_semantics 16.5 s, unit_funpat_free_argument 4.5 s,
  unit_funcpat_narrowed 2.5 s).  No report on any goal, and the known-good
  tests assert the same steps and forks with the flag on and off.  The
  required files on both backends (./run_tests -j 2 --mem 4 --backend both
  unit_cxx_checker.py unit_py_checker.py unit_setfunctions_bugs.py
  unit_setfunctions_bindings.py unit_loadsave.py unit_cxx_tiered.py
  unit_cxx_runtime.py): 14 of 14 run, 0 failed, wall 72.6 s.  make -C docs
  html: build succeeded.
- The review of the lane, four findings.  (1) Confirmed: the C++ checker
  still reported the false S0 (E in A) when a step built more cells than
  the budget of the walk.  The probe: gBig n = case n of { 1 -> [e (((1 +
  0) + ... + 0)), ... 100 elements of ten additions]; _ -> [] }, e i = i ?
  (i + 1), fFirst x = 0 ? (x !! 0), bigThenOutside = let a = gBig (length
  [()]) in let vs = valuesOf (set1 fFirst a) in (head vs, a !! 0, vs).
  The step of gBig at the flagged cell builds about 2300 cells; the walk
  flagged the cells near the result, ran out, and left the cell of e 1
  without flags, so the choice the outside a !! 0 made carried no tag and
  the second alternative of the capsule pulled it across the box: "S0 (E
  in A) violated at escape-set insertion: identifier 1 enters the escape
  set of set #1 but is not argument-derived for it (tags [])", root {box
  #1|(?1 1 (plusInt 1 1))}, counts steps 32, forks 2,
  propagate_over_budget 1, violations 1, compiled and under interpret:all
  (a form with four additions per element, about 1100 cells, stayed under
  the budget on the C++ backend and over it on the Python backend).  The
  Python checker counted the walk over budget and reported nothing: its
  runtime forks on the choice inside the capsule after the enclosing
  configuration decided it (escapes 1, no insertion).  The fix is the
  fallback of the entry walk: a propagation walk over its budget inserts
  the sets of the flags into unbounded_sets, so the checks of S0 of those
  sets are suppressed, not reported, and the counter stays.  After the
  fix both backends give the flag-off values (0, 1, [0, 1]) and (0, 2,
  [0, 2]) with propagate_over_budget 1 and no violation (C++ steps 38,
  forks 5; Python steps 38, forks 5).  The budget stays at 1024 cells per
  event; a budget per event kind is the owner's.  (2) Confirmed:
  twoCapsulesChoice2 and capturedThenShared2 existed in the prose alone.
  Both are in SHARED_ARGUMENT of the two test files now, in the form of
  the review: twoCapsulesChoice2 = let a = g2 (length [()]) in let vs =
  valuesOf (set1 f2 a) in let ws = valuesOf (set1 f2 a) in (head vs, head
  ws, fromJust' a, vs, ws), values (0, 0, A, [0, 1], [0, 1]) and (0, 0, B,
  [0, 2], [0, 2]); capturedThenShared2 with h2 x = case x of Just y ->
  case y of { A -> 10; B -> 20 } and ws = valuesOf (set1 h2 a), values
  (0, A, [0, 1], [10]) and (0, B, [0, 2], [20]).  Under the checker on
  both backends: no report, the same values (C++ steps 44 and 46, forks 9
  and 7; Python steps 42 and 40).  Whether they match the programs the
  earlier lane ran is not known; these are the pinned ones.  (3)
  Plausible, applied: a step that forwards the redex to a static node
  (Fail, True, False) seeded the walk with it, so the static node entered
  the flags map and was rooted, and every later such step ran subset and
  add_all over its growing list.  The walk skips a leaf value now (a
  constructor or Fail without a successor cell), at the seeds and in the
  walk; the flags of such a node are never read (flags_of is read for a
  redex, the source of a pull-tab, a free variable and the root of a copy,
  which hold a successor cell).  Measured on QueensSet (main = queens 8)
  under backend:cxx,checker:true, steps:65536, two runs each: without the
  skip and the fallback wall 20.9 and 24.0 s, peak_rss 160.7 and 160.0 MB;
  with them 20.5 and 22.0 s, 160.0 and 159.9 MB; steps 2089740, forks
  511430 in all four.  No measurable difference on this search.  (4)
  Confirmed, outside the diff: a literal list of 1200 elements fails at
  import with RecursionError on both backends.  bindingopt recurses three
  frames per cons (rec -> comb -> many); with the limit at 4000 the
  visitor-based conversion that follows hits it next (otherwise(*terms)),
  and at 20000 the C stack overflows (8156 kB).  Not fixed in this lane:
  filed as #125 with the reproducer, under the standing approval.
- The tests after the review.  TestSharedArgument of both files holds
  SHARED_ARGUMENT (three shapes) and BIG_STEP (bigThenOutside, built by a
  join in the test file): test_values and test_big_step_values pin the
  flag-off values on both backends; test_under_the_checker asserts the
  values of the three shapes under the checker; test_big_step_under_the_
  checker runs bigThenOutside through a runtime state and asserts the
  values, propagate_over_budget >= 1 and no violation in the counts.  The
  C++ test failed with the S0 report on the build without the fallback
  (verified by a rebuild without the two walk changes, the run, the
  restore and the rebuild); the Python test passes before and after the
  fallback, since its runtime never inserted the choice, and pins the
  count.  Verified after the review: ./run_tests -j 2 --mem 4 --backend
  both unit_cxx_checker.py unit_py_checker.py unit_cxx_runtime.py, 6 of 6
  run, 0 failed, wall 40.1 s (unit_cxx_checker.py 57 tests on cxx,
  unit_py_checker.py 46 on py); the seven known-good files under
  backend:py,checker:true 7 of 7, 0 failed, wall 32.5 s; under
  backend:cxx,checker:true 7 of 7, 0 failed, wall 22.5 s; under
  backend:cxx,checker:true,interpret:all 7 of 7, 0 failed, wall 90.1 s.
  make -C docs html: build succeeded.
- Left to the owner.  (1) The identifier seq_Info and its step seq_step
  name the &> of the runtime; a rename to the name of the symbol
  (BindingGuard) is cosmetic and touches constr.cpp, prelude.hpp and
  rts_bindings.cpp.  (2) The step counts of the C++ backend under a
  compiled Prelude fell by four for a goal that applies a binding at a fork
  (35 to 31 for sibling); the counts under interpret:all and on the Python
  backend are unchanged, so a harness system that compares the compiled
  and the interpreted counters would have found the overwrite.  (3) The
  linkage test guards the two modules of the library with built-ins; a
  general guard would hide the tables the runtime uses alone
  (visibility("hidden")) or check every object of the installed library.
  (4) tests/manifest.json has no entry for the new tests of
  unit_cxx_runtime.py (a calibration run adds them).  (5) The file
  tests/data/curry/ReloadBinding.curry is new and untracked.  (6) The
  budget of the propagation walk, 1024 cells per event, is one constant
  for the steps, the pull-tabs, the copies and the generators; a larger
  budget for the steps alone, or a budget by the size of the result, is a
  policy of the instrumentation and the owner's.  (7) #125, the recursion
  of the ICurry port over a long literal list.

2026-10-09: the port of icurry on a deep expression (#125)
- Symptom: a module whose function returns a literal list of 1200 elements
  failed at import on both backends with RecursionError (the reproducer of
  the issue, LongList.curry).  The front end writes the list as a nest of
  cons calls, one application per element, and the case on the literal 1
  is a Boolean equality in FlatCurry (=== through apply), so the binding
  optimization rebuilt the body of g.  A module with 100 elements imported.
- Cause: every walk over an expression on the route recursed once per
  nested application.  In the port (toolchain/flat2icurry): bindingopt
  (rec, comb, many: three frames per element, the first to fail at the
  default limit of 1000; contains_equality, two), elimnewtype (elim_exp,
  subst_var), casecompletion (complete_exp, two), caselifting (lift_exp,
  unbound_vars), compiler (to_iexpr, var_pos), flatcurry (_decode,
  all_vars) and terms (_show, _show_haskell); the readers and the writers
  ran under maxrecursion (16384 frames), the passes did not.  After the
  port, the reader of the ICurry text: utility.readcurry.parse_ (four to
  five frames per element, at the default limit) and icurry.readcurry
  (Decoder through fmap and the visitation, fourteen frames per element
  under maxrecursion, which ran out at 1200 elements; at a limit of 20000
  it overflowed the C stack, the "Stack overflow (used 8156 kB) in
  comparison" of the issue is the issubclass of its visitation).  On the
  C++ backend the plain-variable analysis of both emitters
  (passthrough._Uses.expr) and the bytecode emitter of the interpreter
  (bytecode._Emitter.expr, three frames per element: the tiered default and
  interpret new and all).  On the Python backend the generated module
  itself: one nested call per element, which the parser of Python refuses
  past 200 parentheses ("too many nested parentheses").
- The fix of the walks.  A new helper, curry.utility.trampoline: a walk is
  a generator that yields the walk of each subexpression and returns its
  result, and trampoline runs the generators on a list, so the depth of
  the tree costs no frame of Python; the code of a pass reads as the
  recursion did, with yield in place of the call, and the side effects
  keep their order (the counter and the order of the lifted functions of
  caselifting, which lifts a new function on the same stack before it adds
  it).  The rebuilders use it: transform_exp, elim_exp, subst_var,
  complete_exp, lift_exp, unbound_vars, to_iexpr, flatcurry.decode and
  icurry.readcurry.Decoder.  The folds keep an explicit stack:
  contains_equality, all_vars, var_pos, showterm and showhaskell (the text
  to emit after a term is an item of the stack), passthrough._Uses.expr and
  bytecode._Emitter.compound (the arguments pushed before the node, left to
  right, as before; make emits MAKE, MAKE_PARTIAL or the choice).  The
  parser of readcurry keeps a stack of frames (an open parenthesis or
  bracket with its closed expressions and the terms under way; a comma
  ends an expression, the closing delimiter ends the frame); make_expression
  is unchanged, so the empty tuple, the nullary application in argument
  position, the infix application and the trailing comma read as before.
  The maxrecursion wrappers of the port (flatcurry.read and decode,
  all_vars, the two writers, compiler.flat2icurry) and of
  icurry.readcurry.loads are gone: no walk of the port or of the reader
  recurses per element.  The output is byte-identical: unit_flat2icurry.py
  (the sample and the probes) and func_flat2icurry.py (the whole archive)
  pass unchanged, with ROUTE_VERSION 1 and the ICurry cache keys as they
  were.  The times of the two files are the manifest's (func_flat2icurry
  27 to 28 s against 26 to 30 s).
- The fix of the Python backend.  CompilerBase hoists a node that lies
  HOIST_DEPTH levels under its statement, and at every multiple of it,
  into a temporary assigned before the statement (hoist, hoisting,
  compileArgs, vEmitHoisted; the temporaries are _h0, _h1, ... of the step
  function).  The Python backend sets 64 (a node is one call, and the
  parser of Python allows 200); the C++ backend sets None and hoists
  nothing.  The arguments of a call are compiled as a list, not a lazy
  generator, so the depth is counted; the symbol imports happen in the
  same order and the generated text of every existing module is the same
  (unit_cxx_emit.py and the files that read generated code pass), so the
  FORMAT_VERSION of neither backend moved: a module with such an
  expression never compiled before.  Under interpret:off the module of the
  issue compiles with g++ (19 s, 794 MB peak) and prints the same values.
  The digest of the generator sources (cxx/toolchain.GENERATOR_SOURCES
  names passthrough.py, generic/compiler.py and icurry/) changed, so the
  product cache misses once per module; the test files that read the
  compiled object of a pool module (unit_cxx_bom.py, unit_cxx_partial.py,
  unit_cxx_passthrough.py) fail with KeyError 'cxx.shlib' on the stale
  products under the tiered default until the prepare pass runs, as the
  README says after a change to a code generator (./run_tests --prepare).
- Bounds left, under maxrecursion (16384 frames).  A literal list of 2500
  elements imports on both backends; one of 2800 (and 3000, 5000, and 5000
  nested additions) fails at import on both with RecursionError, 16384
  frames: 8181 copy.deepcopy, 2729 _reconstruct, 2729 _deepcopy_dict and
  2723 _deepcopy_list under optimize -> inline_calls -> inline_shape
  (icurry/analysis/inlining.py, body_shape(copy.deepcopy(ifun.body),
  arity)).  The deep copy costs six frames per nested application, so the
  first walk to fail past 2500 elements is the copy of the function body
  in the inliner, at about 2700 elements.  Behind it, measured with a
  profile hook over the import of the module of the issue: the optimizer's
  visitors over the ICurry (icurry.visit under inline_aliases,
  saturate_applies, set_monadic_metadata and find_static_strings: about
  three frames per nested application, 3657 at 1200 elements, so about
  5400 elements), inlining.node_count and _valid_expr (two per level), the
  generic compileE of both backends (four frames per level, 4857 at 1200
  elements on the Python backend, so about 4000 elements), and the JSON
  codec of the ICurry, which recurses in C: 5000 levels pass in the main
  thread, 20000 overflow its 8 MB stack.  A second shape, untouched by the
  fix: a chain of nested if-then-else (f x = if x == 1 then 1 else (if x ==
  2 then 2 else ...)) of 1200 levels translates (the lifting puts each
  case in its own function; the port exits 0 at the default limit) and
  fails at import on the C++ backend in set_monadic_metadata: the analysis
  follows the chain of lifted functions, about seven frames per level
  (4778 of visitation.replacement, __call__ and visit.visit each, 682 of
  _checkmonadic), so about 680 levels; a chain of 400 prints [3].  A long
  guard or if chain is the same shape.
- Tests, each at the default recursion limit of 1000, each failing with
  RecursionError (or the SyntaxError of the Python backend) before the fix:
  TestDeepTerms of unit_flat2icurry.py (the parser, the decoder and the two
  writers, the selectors and the passes, and the translation, on a list of
  20000 elements; var_pos on 2000 and unbound_vars on 5000, the quadratic
  ones); test_deep_expression of TestPass in unit_flat2icurry_bindingopt.py
  (a chain of 20000 conjunctions in a condition, every equality replaced;
  a list without an equality rebuilt as it is); ReadCurryDepth of
  unit_parse.py (the parser and the decoder on 20000 nested calls);
  test_deep_expression of TestEmitter in unit_cxx_interp.py (the bytecode
  of a list of 20000: PUSH_CONST and MAKE 20000 each, RET_NODE last,
  nstack 20001); TestDeepExpressions of unit_curry2icurry.py (the modules
  of the issue, LongList with 1200 elements and DeepSum with 1200 nested
  additions, written by the test into a temporary directory with the
  ICurry cache off, imported and evaluated to [1, 2] on the backend of the
  run, under interpret:new: under the tiered default each import started a
  background g++ of the module, 18 s and 794 MB, that ran beside the rest
  of the file and was killed at exit; the tests pin the import, the
  emitters and the evaluation, and the compile with g++ is the
  interpret:off run above).  The script of the issue prints [1, 2] on both
  backends.
- The runs (the clean environment, CCACHE_DIR set, ./run_tests -j 2 --mem 4
  --backend both).  The oracle and the toolchain: unit_flat2icurry.py,
  func_flat2icurry.py, unit_flat2icurry_bindingopt.py, unit_curry2icurry.py,
  unit_curry2icurry_stale.py, unit_parse.py: 12 of 12 run, 0 failed, wall
  93.6 s.  The rest of the toolchain tests and the checkers: unit_goals.py,
  unit_sigtable.py, unit_typecheck_serialize.py, unit_typed_expr.py,
  unit_typecheck.py, unit_runner.py, unit_py_checker.py,
  unit_setfunctions_bugs.py: 16 of 16, 0 failed, wall 65.6 s.  The
  emitters and the optimizer: unit_cxx_interp.py, unit_cxx_emit.py,
  unit_cxx_tiered.py, unit_cxx_passthrough.py, unit_cxx_partial.py,
  unit_cxx_bom.py, unit_optimize.py, unit_optimize_inline.py,
  unit_optimize_applies.py, unit_icurry.py, unit_py_interp.py,
  unit_loadsave.py, unit_reload.py, unit_prebuild.py, unit_product_cache.py,
  unit_cxx_toolchain.py: 32 of 32, 3 failed, wall 121.2 s, the three
  stale-product files above; with --prepare (data/curry and the benchmarks
  on both backends, 2.5 to 34.5 s each) those three: 6 of 6, 0 failed, wall
  75.5 s.
- Left to the owner.  (1) The deep copy of the function body in
  inline_shape (six frames per nested application, the bound of about 2700
  elements), the optimizer's visitors (icurry.visit, about 5400 elements;
  set_monadic_metadata over a chain of lifted functions, about 680
  levels) and the generic compileE (about 4000 elements) recurse per level
  under maxrecursion; an iterative copy (a __deepcopy__ of the ICurry
  nodes on a stack, or a shape computed without a copy), an iterative
  visit and an iterative compileE would lift them.  (2) The JSON
  codec of the ICurry recurses in C.  (3) HOIST_DEPTH is a constant of the
  Python emitter, not a flag; the C++ emitter writes one nested call, which
  g++ accepts at 1200.  (4) rc.show, Applic.__eq__ and __repr__ of
  readcurry and Term.__eq__ of the port recurse per level; the oracle's
  structural difference and the tests of equal deep terms use them under
  maxrecursion.  (5) tests/manifest.json has no entry for the new tests
  (a calibration run adds them; unit_flat2icurry.py takes 16.5 s, 12 s
  before).  (6) The new module utility/trampoline.py needs make stage (or
  make install in src/python/utility) to appear in a staged tree.

2026-10-10: the py shard of CI moves to the nightly (#82, stage 4, second half; worktree wt/pynightly)
- The change.  The matrix of the job test in .github/workflows/ci.yml
  keeps the three cxx shards and drops the py entry.  A job of its own,
  python, runs the Python backend right after the test job.  Its display
  name stays "py backend 1/1", so the job list of the nightly reads as
  before.  Its condition is that of the stress job: the nightly schedule,
  or a workflow_dispatch with the new input python (a Boolean, default
  true, listed after functional).  The steps are those of the test job for
  the py entry: a build without icurry (--with-icurry=""), one shard, the
  ccache and ICurry cache keys the matrix wrote (ccache-py-1/1-... and
  icurry-cache-py-1/1-..., written out), the functional step under the
  condition of the test job, the save of the ICurry cache and the ccache
  statistics.  The comment above the matrix and the header of the job say
  why.
- The reason.  The second half of stage 4 of #82 (the py shard leaves the
  pull-request matrix and runs on the nightly as a parity job) was
  deferred to the month of the removal gate.  The owner decided on
  2026-10-10 to move it now and to freeze the Python backend: no new
  features, and no parity work for new semantics unless a pair of the
  differential harness or a test needs it.  Its suites run on the schedule
  and on request, not on every push.
- What runs now.  A push to master, py314 or a wt/** branch, and a pull
  request to master: the three cxx shards with the unit suite, and the
  docs job (deploy docs on a push to master).  The nightly: the three cxx
  shards with the unit and functional suites, py backend 1/1 with both
  suites, the collector stress job, interpret:all and interpret:off, and
  the docs job.  A dispatch: the cxx shards always; the functional suites
  (functional, default true), the py job (python, default true), the
  stress job, the interpreter jobs and the write-counters job by their
  inputs.  The removal gate of #82 is unchanged: a month of green nightly
  runs under interpret:off, tiered and all on the C++ backend, and no open
  bug that names the Python backend as its only reproduction.
- Docs.  docs/source/DeveloperNotes.rst: the oracle bullet of "What it is
  for" names the nightly job in place of the pull-request matrix, the
  alias sentence names the CI workflow in place of the CI matrix, and the
  removal gate names the CI job in place of the CI shard.  tests/README,
  AGENTS.md, conda/README.md and the CI scripts name no py shard and are
  unchanged.
- Not run here: no build and no CI run.  To check on GitHub: the push run
  of the branch shows the cxx-only matrix; a dispatched run with
  python=true shows the job py backend 1/1.

2026-10-10: the default of setfunction_failures is 'escape' (decision D1 of
  the memo on the Fair Scheme proofs, confirmed by the differential harness;
  worktree wt/flip)
- - The decision and its evidence.  The flag setfunction_failures (the lane of
  2026-10-09 above) selects what a failure that comes from an argument of a
  set function does when the capsule demands it: 'escape' fails the set
  function (option F2 of the memo: the semantics of weakly encapsulated search
  of Christiansen, Hanus, Reck and Seidel, PPDP 2013, which KiCS2 implements),
  'encapsulate' drops its alternative (rule SF.5 of the dissertation).  The
  lane left the default at 'encapsulate' until the differential harness
  confirmed the decision.  Campaign 2 of the harness of the proofs project
  confirmed it on 2026-10-09: 200 FS-S goals against KiCS2, agreement 192 of
  200 under 'encapsulate' and 194 of 200 under 'escape', and no other verdict
  moved.  The owner approved the flip on 2026-10-10, in a lane of its own.
  The default is 'escape'.  The flag stays, with 'encapsulate' as the
  alternative.  The semantics of neither setting changes: 'escape' keeps the
  immediate trigger of the lane (the first demand of a boxed failure inside
  the capsule fails the set function), and 'encapsulate' keeps the rule of the
  dissertation.  No other default, no object format, no public API and no flag
  changes.
- - The places that stated the default; every one moved.
  src/python/interpreter/flags.py: FLAG_INFO, and the docstring, from which
  the reference page of the flags is generated (the sentence that the default
  moves later is gone: 'escape' is the decision D1, the default since the
  harness confirmed it, and 'encapsulate' is the semantics of the
  dissertation).  docs/source/PythonAPI/CurryPackage.rst: the row of the table
  of the flags.  docs/source/CurryPrimer/Semantics.rst: the paragraph on a
  failure that comes from an argument (isEmpty (set1 id failed) has no value,
  and is True under 'encapsulate'). curry/lib/Control/SetFunctions.curry: the
  comment of applyS.  A comment alone: as the README of the library
  prescribes, the .icy made from the edited source with sprite-make --icy (in
  a scratch tree, with SPRITE_DISABLE_SYSLIB_CHECKS=1 as make -C curry sets
  it) is byte-identical to the committed one, and the digest of the source in
  tests/unit_currylib.py is updated.  src/cyrt/state/rts.hpp: the default
  argument of the constructor of RuntimeState, the one default of the C++
  runtime's own when the flag is absent, is SETF_FAILURES_ESCAPE, under a
  comment that the defaults of the arguments are those of the flags (the
  bindings pass the flag always, so no behaviour moved there; the header is in
  the include closure of cyrt.hpp, so make stage built the runtime and the
  library again, and the prepare pass compiled the test products again; the
  edit moves the ABI stamp, item (6) below).  No default of its own elsewhere:
  the bindings map the value of the flag (cyrtbindings/__init__.py), the
  Python backend reads interp.flags (eval/rts.py), and the comments of
  rts_setfunctions.cpp, currylib/setfunctions.cpp, fwd.hpp,
  rts_setfunctions.py, fairscheme.py and control.py name no default.  The
  REPL's :set lists its two options and no flag; the developer notes, the
  examples and their READMEs name no default of the flag; conda/ names no
  flag. tests/data/curry/SetFunctionsSemantics.curry: the comment of section
  3. tests/unit_setfunctions_semantics.py: the docstrings name the default,
  and TestDefault pins it (below).  The entries of this file older than this
  one keep their statements, as history.
- - Tests.  No expectation of a set-function test moved: the tests that run a
  goal whose values differ between the settings set the flag explicitly and
  run both settings, so both settings keep their tests (the memo's s3, s3e,
  s4, s4h, s4a, s4b, s4c, nestFail and nestFailInner in
  unit_setfunctions_semantics.py; the shared shapes of section 6 of
  SetFunctionsBindings.curry in unit_setfunctions_bindings.py; the semantics
  module in unit_py_checker.py and unit_cxx_checker.py).  The functional
  corpus of set functions holds no failing argument, and the examples give
  their outputs on both backends.  New: TestDefault of
  unit_setfunctions_semantics.py pins the default (FLAG_INFO and
  get_default_flags) and the values of s3, s3e, s4a, nestFailInner and
  lazyFirst in a child that sets no flag (no value, no value, no value, True,
  True).  The child inherits SPRITE_INTERPRETER_FLAGS, so it prints its
  setting first, and the test asserts 'escape' before the values, with a
  message that names the environment: a run with
  setfunction_failures:encapsulate in the environment fails with the cause
  (the review found a value diff without one).  One test asserted the old
  default's values: test_counters_of_a_search of unit_cxx_rotation.py pinned
  the steps and forks of countQueens 8 of CxxGc.curry under the cadence of
  65536 steps, (2112920, 511484); under the new default they are (2020742,
  461457), with the same value, 92.  The cause: a permutation of the program
  holds a failure (the second rule of ndinsert has no case for []: an
  overlapping rule), the capsule of unsafe demands it, and under 'escape' that
  demand fails the set function at once, where 'encapsulate' ran the other
  alternatives of the capsule first.  A scratch module confirms it: the same
  search with an insertion that has no failing alternative takes the same
  steps and forks under both settings (555018 and 76415 for the board of 7),
  and the one with ndinsert takes 373018 and 82612 under 'escape' against
  391282 and 92178 under 'encapsulate', 40 values both.  The test asserts the
  counters of the default now, and a sibling test under 'encapsulate' keeps
  the old pin (COUNT_QUEENS_COUNTERS).  The benchmark QueensSet moves the same
  way (the Python API, the compiled module, step mode): 1997870 steps and
  461545 forks under the default, 2089740 and 511430 under 'encapsulate' (the
  numbers of the record of the lane of 2026-10-09), 92 values both; the wall
  is 0.5 to 0.7 s under both, on a shared machine.
- - The CHRS13 probe, a documented result and not a fix.  The example of
  Christiansen, Hanus, Reck and Seidel on the level of a failure is set1 (\a
  -> a ? True) failed, for which the paper gives the set {True}. The scratch
  module, outside the tree, defines probe = sortValues (set1 (\a -> a ? True)
  failed) and probeNE = notEmpty (set1 (\a -> a ? True) failed).  sprite-exec
  -g probe, step mode, prlimit 2 GB, the three results verbatim: the C++
  backend under the new default prints nothing and exits with status 0 (no
  value); the C++ backend under setfunction_failures:encapsulate prints
  [True]; the Python backend under the new default prints nothing and exits
  with status 0 (no value).  Two more rows: probeNE on the C++ backend under
  the new default prints nothing (no value), and probe on the Python backend
  under 'encapsulate' prints [True].  Through the Python API on the C++
  backend, list of curry.eval(M.probe) is [] under the default and ['[True]']
  after a reload with 'encapsulate'.  So the immediate trigger differs from
  the paper's example: the alternative a of the body demands the boxed
  failure, and the trigger fails the whole set function at that demand, where
  the paper drops the alternative a and keeps True.
- - Verification.  Before the change, on the clean head (the worktree had no
  compiled products, so the C++ backend ran the modules interpreted under the
  tiered default): ./run_tests -j 2 --mem 4 --backend both on the five files
  that name the flag, 10 of 10 run, 0 failed, wall 49.0 s (unit_py_checker.py
  46 tests on py, unit_cxx_checker.py 57 on cxx, unit_setfunctions_bugs.py 27
  on each, unit_setfunctions_bindings.py 16 on each,
  unit_setfunctions_semantics.py 41 on each; the checker files skip on the
  other backend).  The log of that baseline run was not kept: the runner's
  logs under tests/.cache/runner hold the runs after the change alone, so the
  baseline rests on this record.  After the change: make -j6 stage under the
  clean environment, exit 0 (the runtime, the bindings and the library built
  again); make overlay, 9 s (it pruned the products of the edited test
  module); ./run_tests --prepare-only for the selection, 89 modules in 2
  directories on cxx+py, every product present, wall 51.6 s; the batch of ten
  files on both backends (the five above with unit_currylib.py,
  unit_cxx_rotation.py, unit_cxx_scheduler.py, func_setfunctions.py and
  unit_examples.py), 20 of 20 run, 1 failed, wall 157.1 s: the counters test
  above, and the rest ok (unit_setfunctions_semantics.py 43 tests on each
  backend, unit_examples.py 22 on each, py 103.1 s and cxx 22.0 s,
  unit_currylib.py 10 on each with the new digest, unit_cxx_scheduler.py 9 on
  each, func_setfunctions.py 39 on each).  After the edit of the counters
  test, one file per process: unit_cxx_rotation.py 21 tests ok on cxx and on
  py (14 skipped there), unit_setfunctions_semantics.py 43 ok on cxx and on py
  (2 expected failures there).  After the review's edit of TestDefault:
  unit_setfunctions_semantics.py 43 ok on cxx and on py (2 expected failures
  there) one file per process, and the class TestDefault alone under
  SPRITE_INTERPRETER_FLAGS=backend:cxx,setfunction_failures:encapsulate fails
  one test with the message that names the environment.  The .icy of the
  edited library source: sprite-make --icy in a scratch tree, byte-identical
  to the committed file (cmp).  make -C docs html: build succeeded, no
  warning; the generated reference page of the flags, the table of the Python
  API page and the primer carry the new default.  Nothing is committed from
  this lane.
- - Left to the owner.  (1) The trigger.  The probe shows that the immediate
  trigger gives the paper's example no value where CHRS13 gives {True}: the
  level of the failure.  A trigger at the level of the alternative, which
  drops the alternative that demands the boxed failure and fails the set
  function when every alternative did, is one reading of the paper; its
  design, and whether 'escape' should mean it, is the owner's decision.  (2)
  The counters.  Every search whose boxed argument holds a failing alternative
  takes fewer steps and forks under the default (QueensSet above; QueensSet9,
  QueensSet10 and the other benchmark programs with set functions were not
  measured here), so the next run of run_benchmarks against the records moves
  those counters; the records are not updated here.  (3) tests/manifest.json:
  the new tests add about a second to unit_setfunctions_semantics.py (2.0 s)
  and to unit_cxx_rotation.py (9.7 s on cxx); a calibration run updates the
  entries.  (4) The items of the lane of 2026-10-09 stand: the eager strategy
  loops on s1 and s6PL, the Python backend's known failure covers capNested
  and nestFail, and holds_private_state copies the capsule for a captured
  choice decided before the start.  (5) The entries of this file older than
  this one state the old default; they are history and are not edited.  (6)
  The ABI stamp.  The edit of rts.hpp (a default argument that no caller uses:
  both constructors the bindings expose take the argument) is in the include
  closure of cyrt.hpp, which object_digest digests: runtime_digest moves from
  549b676a7d379128 to 9ef6d4101fb12503, the stamp of the worktree is
  16175fe95af94904, and an installation that takes this commit compiles every
  compiled Curry product again, once. The owner can keep the edit (the
  runtime's own default then matches the flag) or drop the unused default
  argument from the stamped header and leave flags.py as the single source of
  the default.

2026-10-10: ($!) of a free variable, getimpl after a swap, the private compile of a foreign budget, and the compiler variables of the runner (#112, #115, #116, #111; worktree wt/bugs7)
- #112, ($!) of a free variable.  Cause: on both backends ($!) ran hnf on
  the argument and handed its suspension on: a free variable at the root
  of the argument recorded a residual (E_RESIDUAL in applyhnf_step of
  src/cyrt/currylib/prelude/apply.cpp; the control exception of
  fairscheme.hnf in apply_hnf of backends/py/currylib/prelude/apply.py),
  where PAKCS 3.4.1 reads the variable as a head normal form and applies
  the function to it (id $! x gives _a).  Fix: the C++ step takes the
  residual back and reports T_FREE when hnf suspended on a free variable
  at the root, as applynf_step does since pothole batch 3 and as
  hnf_or_free does; _applyspecial accepts T_FREE and builds the
  application.  The Python step uses fairscheme.hnf_or_free in place of
  hnf.  ($##) still suspends (applygnf_step).  Probe: id $! (x::Int)
  where x free gives _a on both backends (both suspended before); not $!
  x where x free gives the two values.  Test:
  test_apply_nf_free_variable of unit_prelude.py covers ($!) beside ($!!)
  and normalForm (it ended in EvaluationSuspended on the Python backend
  with the fix reverted; the C++ half was shown by the probe, not by a
  rebuild).  No oracle run: PAKCS writes under /tmp, which the sandbox
  of this lane mounts read-only.  The C++ change is apply.cpp alone (make
  stage, make overlay: status 0).
- #115, getimpl after a swap.  Cause: the swap of a background compile
  recorded the object in the ICurry of the module (cxx.shlib, which
  handle.sofilename reads) only through _refresh(interp, name), and
  poll(interp) ran it only when an interpreter was given.  tiered.wait()
  without one, as the issue calls it, applied the swap and recorded
  nothing; implementation.getimpl then found no object ("its module has
  no compiled object"), and so did every reader of sofilename (the
  loader, Cpp2So.import_lacks_an_object).  The poll of that wait took
  the result, so no later poll of the process, with an interpreter or
  not, recorded the object: getimpl raised before and after an
  evaluation.  A second process showed the C++ because it loaded the
  object at the import.  Fix: submit keeps a weak reference to the
  module object of the job (_state.modules), and _refresh(name, interp)
  records the object on that module whichever thread polls and whether
  or not an interpreter was given; the module of that name in the
  interpreter, when it is
  another object, learns of it too.  A result that did not swap drops
  the reference; cancel clears them.  Test: test_getimpl_after_the_swap
  of unit_cxx_tiered.py (the bytecode before, tiered.wait(seconds)
  without an interpreter, sofilename the object beside the source, the
  C++ with the banner and no bytecode line, equal to M.area.getimpl();
  with the fix reverted it failed at sofilename, None).  The probe of the
  issue prints the compiled form after the wait.
- #116, the private compile of a foreign budget.  Cause: tiered.submit
  ran sprite-make --so on the source beside the shared products with the
  flags of the interpreter (backend:cxx,interpret:off,debug,inline_budget:N),
  by design, so that the code swapped in is the code the interpreter ran
  (test_budget_reaches_the_child).  The budget shapes the generated code,
  the key of the product cache digests it (Cpp2So.product_facts), but the
  ABI stamp beside the object does not, so a process with another budget
  wrote an object every later process of the tree took for current
  (reproduced: a budget-0 process compiled a fresh module in the
  background, and a default process then loaded that object at the
  import, interpreted nothing, and took 3 steps for viaCall where the
  inlined code takes 2).  The same flags reached the compile on first
  use under interpret:off (materialize.compile_pending compiled the
  translation beside the source), and they still reach the plan of a
  plain import of a stale module under interpret:off.  Fix, the second
  alternative of the task: a process whose inline budget is not the one
  the environment names (tiered._foreign_flags: the defaults of the
  flags under SPRITE_INTERPRETER_FLAGS, what a child reads on its own)
  compiles a private copy.  _private_copy puts the source, with its
  current ICurry file, interface files and JSON, under
  tiered._tmpdir()/privateN/ in the layout of a search path entry; the
  child runs sprite-make --so on the copy, with the private directories
  of the earlier jobs and the root of the original on its CURRYPATH, so
  an import compiled this way is found with its object before the
  original, and the other imports in place.  The generated file and the
  object land in the private directory, which the product cache leaves
  out (_productcache.excluded) and which goes with the process; the
  digest of the swap's check stays the one of the ICurry file beside the
  original.  The first-use path compiles into the process directory
  under a foreign budget as well (process_file, as a transformed object
  does).  The budget of the environment compiles beside the source as
  before, so the prepare pass, sprite-make and a run under
  SPRITE_INTERPRETER_FLAGS=inline_budget:N agree with themselves.
  Probes: the budget-0 process left nothing beside the source, swapped
  to its private object (3 steps for viaCall before and after), and the
  default process interpreted the module, compiled it beside the source
  and took 2 steps on both tiers; Tak1 of the benchmarks corpus with no
  object beside the source, under budget 0: interpreted, compiled into
  privateN/, nothing beside the source (nm of the private object shows
  no cyrt::Variable::skip reference: the symbol count of the issue came
  from the flags of #111, not from the budget).  Tests:
  test_foreign_budget_compiles_a_private_copy of unit_cxx_tiered.py (the
  steps of each tier under budget 0, nothing beside the source, the
  object and its stamp under tiered._tmpdir(), getimpl reads it; then the
  same module under the budget of the environment is interpreted at the
  import, compiled beside the source, and inlined on both tiers; with
  the fix reverted the default process loaded the foreign object) and
  test_foreign_budget_compiles_into_the_process_directory of
  TestFirstUse in unit_cxx_interp.py.  The alternatives, for the record:
  (a) the background compile of a shared module under the flags of the
  environment, which swaps in code of another budget than the
  interpreter ran (the defect test_budget_reaches_the_child pins);
  (b) the private copy, taken; (c) no background compile under a
  foreign budget, which leaves such a process on the interpreter; (d)
  the stamp or the record carries the optimizer keys, which changes the
  stamp (the owner's; see #111 below).  Left to the owner: (1) a process
  with a foreign budget still loads a current shared object of the
  default budget (the Tak1 probe before its object was removed: the
  "reuses" half of the issue), and a plain import of a stale module
  under interpret:off still compiles beside the source with the flags of
  the process; both end with the stamp or the record naming the budget.
  (2) The private copy runs the front end again when the module has no
  products beside its source (a read-only directory); the products are
  copied when they exist.  (3) debug:true compiles a debug object beside
  the source as before: its stamp names the flavor, so a release process
  compiles it again rather than runs it.
- #111, the compiler variables of the runner.  Cause: cli.environment
  copied os.environ into the environment of every test process and of
  the prepare pass, and Cpp2So._cxxflags appends CXXFLAGS of its
  environment to the compile command (configure and the Makefiles honour
  the variables by design); the compiler itself is the installation's
  (config.cxx_tool is tools/cxx) whatever CXX says.  So a login shell
  with a conda toolchain compiled the corpus under -O2 -fno-plt
  -ffunction-sections and named the precompiled header O3O2-...
  (reproduced with the variables set: member O3O2-13a51c520ba2, the
  flags in the command).  Fix: environment() drops TOOLCHAIN_VARIABLES
  (AR, AS, CC, CFLAGS, CPP, CPPFLAGS, CXX, CXXFLAGS, LD, LDFLAGS, LDLIBS,
  NM, OBJCOPY, OBJDUMP, RANLIB, READELF, STRIP) from the environment of
  a child, which covers the test processes, the prepare pass, the
  sprite-make -S probe and the prune of the product cache; main prints
  one line before the first file when the environment of the run sets
  one ("toolchain: the environment sets CXX, CXXFLAGS; the test
  processes and the prepare pass compile under the compiler and the
  flags of the installation instead").  No opt-in: the issue proposed a
  flag, which is the owner's; a file run by hand still reads the shell.
  tests/README section 10 says so.  Tests in TestCli of unit_runner.py:
  test_environment_drops_the_toolchain_variables (the variables gone,
  the others kept, the note's text, the note once in a --list run) and
  test_child_compiles_under_the_installation_toolchain (a child of the
  installed python under the environment of the runner from a shell
  that exports the conda names: the compiler is tools/cxx of the
  installation, no flag of the shell in Cpp2So._cxxflags, the member of
  the flavor, no CXX or CXXFLAGS in its environment; with the fix
  reverted, -ffunction-sections was in the flags).  Left to the owner:
  whether the ABI stamp records the compiler flags (the second cause of
  the issue; a leaked object passes for current until the stamp or the
  record names them), and the tests that read CXXFLAGS of their own
  environment (unit_prebuild.py, the two tiered tests that fail a
  compile through it) keep doing so.
- Verification (env -i, prlimit, CCACHE_DIR set, one or two processes at
  a time).  make stage and make overlay after the C++ change: status 0.
  Single files: unit_prelude.py 18 ok on cxx and on py (1 skipped),
  unit_cxx_tiered.py 29 ok (46 s), unit_runner.py 71 ok (17 s),
  unit_cxx_interp.py 38 ok (32 s).  The new tests verbosely, each ok and
  none skipped; with the Python side of the fixes reverted, each failed
  (unit_prelude on py: EvaluationSuspended; the runner's child test:
  -ffunction-sections in the flags; the getimpl test: sofilename None;
  the others: the helpers missing).  The prepare pass: the pool, 49
  modules in 33.5 s, every product present; the benchmarks corpus, 60
  modules in 34.0 s, every product present.  Through the runner
  (./run_tests -j 2 --mem 4 --backend both) over unit_prelude,
  unit_cxx_tiered, unit_runner, unit_cxx_interp, unit_inspect,
  unit_optimize, unit_optimize_inline, unit_optimize_applies,
  unit_cxx_steplimit, unit_cxx_heap, unit_cxx_toolchain,
  unit_product_cache, unit_loadsave, unit_api, unit_goals and
  unit_prebuild: 32 of 32 run, 0 failed, wall 136.1 s (unit_cxx_heap.py,
  the file of the issue, 10 ok on cxx after the prepare pass).
- Review pass (the same day).  (1) The cause sentence of #115 above
  said the next Interpreter.eval repaired the record; it did not, the
  poll of the wait had taken the result (reworded above).  (2) An
  inline budget in SPRITE_INTERPRETER_FLAGS of a run reaches the
  children (with_backend keeps the other flags), which then compile the
  shared products under it with nothing said: the leak of #116 for
  every later run under another budget.  Until the stamp question is
  settled, main prints one line that names the budget (cli.budget_note,
  beside toolchain_note; flag_value is the reader both notes share;
  tests/README section 10; test_budget_note of unit_runner.py).  No
  flag and no change to the stamp.  (3) The search path of the child of
  a private compile listed the private directories oldest first, so a
  reload of M with its shape kept (a new private copy) left an importer
  compiled later to find the older copy of M first; no wrong behaviour
  was seen (the shim binds the symbols and the tables hold the live
  steps), but the docstring promised the live copy.  Now the newest
  first (test_private_copies_newest_first of unit_cxx_tiered.py: three
  copies of one module, the third's search path is [dir2, dir1, root];
  it failed on the order with the old line).  (4) compile_pending ran
  is_translation (a read of the JSON and the optimizer passes) before
  the test of _foreign_flags and threw the answer away under a foreign
  budget; the cheap test comes first now.  Verification: unit_runner.py
  72 ok (17 s), unit_cxx_tiered.py 30 ok (47 s), unit_cxx_interp.py 38
  ok (28 s), one file at a time; ./run_tests --list under
  SPRITE_INTERPRETER_FLAGS=inline_budget:0 CXXFLAGS=-O2 prints the
  toolchain line and the inline budget line, once each.
- Left to the owner, across the issues: the stamp question of #111 and
  the two halves of #116 that only the stamp or the record settles (the
  note of the runner names a budget of the environment until then); the
  whole suite on both backends (not run in this lane: longer than its
  foreground bound).

2026-10-10: curry.save of a module loaded from its object, a module of the same name after a reset while the old object is alive, and the eager set-function strategy (#113, #114, #118; worktree wt/bugs8)
- #113, curry.save of a module loaded from its object.  Cause, three
  layers.  (1) CxxCompiler.vIsBuiltin asked Module::get_builtin_symbol
  and get_builtin_type, which answered "a static table" (is_static,
  F_STATIC_OBJECT).  The tables of a module loaded from its compiled
  object are static too, so the compiler took every function and type of
  such a module for a built-in of the runtime and wrote an extern
  declaration in place of each step function (reproduced with a module of
  42 step functions: 45,080 characters and 870 lines saved interpreted,
  6,122 characters, 126 lines and no step function saved from the
  object).  (2) compilable_icurry of interpreter/loadsave.py read the
  JSON for the bodies and ran neither the merge of the built-ins nor the
  passes of the optimizer, so the step functions would have been compiled
  from the unoptimized bodies.  (3) With the passes run on the complete
  ICurry, inline_calls looked the module up by its name among the loaded
  modules and found the bill of materials, where no function has a body
  or lies on a cycle, so it recorded the recursive count and its case
  function as inline bodies: two metadata entries the interpreted save
  did not write.  Fix: get_builtin_symbol and get_builtin_type answer
  from the registered built-in modules (g_builtin_modules of
  cyrt/module.cpp), never from the static flag (Module::adopt still
  clears F_STATIC_OBJECT on a swapped table; harmless); compilable_icurry
  takes the JSON through mergebuiltins, validatemodule and
  interp.optimize, with the complete ICurry standing in for the loaded
  module in interp.modules and in the ICurry of its package while the
  passes run (_standing_in).  Probe: the two texts are byte-identical
  after the fix (45,080 characters, 42 step functions).  getimpl of a
  function that the generated file of such a module lacks now says "it is
  not defined in" rather than "a built-in of the C++ runtime".  Test:
  test_same_text_from_the_object of TestSaveFromObject in
  unit_loadsave.py (two children: the first under interpret:new before
  any object exists, then sprite-make --so, then the second under
  interpret:tiered from the object; the step counts and the texts must
  agree; with the C++ fix reverted and the runtime rebuilt it failed with
  'steps 0' != 'steps 42').  No change to what save writes: C++ source,
  as decision 6a of 2026-10-08 leaves it.
  Regression of this change, found by the suite of the batch and fixed the
  same day: test_reload_from_tiered_into_interpreter of
  TestReloadAfterCompiledObject in unit_cxx_runtime.py found Prelude.so
  mapped after a reload from tiered into interpret:all (['Prelude.so'] !=
  []), where the route from interpret:off passed.  Cause:
  Materializer.materializeEx of an IType asked get_builtin_type alone before
  it made the tables of a type, and the old answer covered the static types
  of a compiled object.  The merge of the built-ins lists thirteen types of
  the Prelude without their material that the runtime does not register
  under the name (the tuples (,,) to (,,,,,,,,,,,,,) and (->)), and the
  record of Prelude.so carries them as static types; with the new answer the
  materializer made a second table per type and constructor (create_type
  replaced the entry of the module with it), and under tiered execution,
  where the Materializer keeps the tables of every module, the clear at the
  reload kept them and the object with them (g_kept_libraries) for the life
  of the process; under interpret:off nothing is kept, the tables were freed
  with the module, and that route passed.  Fix: the materializer takes the
  static type the module has (Module::get_type) when the type is not a
  built-in, as the function path asks get_infotable before it makes a table;
  a type made at run time is not taken, as before (two interpreters of one
  process that load a module of one name make their own types).  Probes:
  after the fix no type of the Prelude loaded from its object is made at run
  time (17 merged types, all static), the reload from tiered into all maps
  nothing and the values hold, and Control.SetFunctions loaded from its
  object had no such table before or after.  Test:
  test_no_table_made_at_run_time of TestLibraryTablesInTheirObjects in
  unit_cxx_runtime.py, which asserts that every type and constructor of the
  Prelude and of Control.SetFunctions loaded from their objects is a static
  table of the runtime's Module; with the materializer before the fix it
  fails with 'the type Prelude.(,,) was made at run time', where test_tables
  of the class passes (its symbol list does not reach those tables).
  Verification: the two tests of TestReloadAfterCompiledObject pass alone;
  through the runner (-j 2 --mem 4 --backend both) unit_cxx_runtime,
  unit_cxx_tiered, unit_cxx_interp and unit_reload: 8 of 8, 0 failed, wall
  40.5 s; unit_compile_modules, unit_loadsave and unit_setfunctions_bugs: 6
  of 6, 0 failed, wall 14.5 s; the five tests of #114 pass by name on cxx,
  and its two expectedFailure tests stay expected failures;
  unit_cxx_runtime.py with the new test through the runner on both backends:
  2 of 2, 0 failed (10 tests each).
- #114, a module of the same name after a reset while an object of the old
  module is alive.  Cause: the registry of the runtime maps a module name
  to one Module object through a weak pointer (Module::find_or_create),
  and the Python module object holds that object as its backend handle.
  A reset unlinked the name from interp.modules and nothing else: while
  the user held the old module object (or the second one, in the third
  step of the issue), the Module stayed alive with its tables and their
  steps, find_or_create gave it to the next module of the name, and the
  materializer, which keeps a table that has a step, attached nothing
  (second.f and third.f gave 1 on the module route; reload the same).  The
  handover designed into Module::clear (the kept store, taken back by name
  through create_infotable and create_type, which the shim of tiered
  execution counts on) ran in the destructor alone.  The import route met
  the same, and its data types a second defect: create_infotable and
  create_type made a second table per constructor and type beside the
  live ones (their assert is off under NDEBUG), so the shim of the first
  incarnation no longer matched, and the module stayed interpreted with
  the warning "the tables of module 'M' changed after compiled code bound
  to them".  Fix: Module::retire (cyrt/module.hpp, bound in
  cyrtbindings/module.cpp) hands the tables to the kept store and clears
  the module at once, with keep forced, because Python objects of the old
  module name the tables (a trapped table of a module never evaluated
  under interpret:off would be freed otherwise); IBackend.module_unlinked,
  a hook that Handle.unlink calls once the name left interp.modules, and
  that the C++ backend implements with retire; Interpreter._unlink_modules,
  the loop of reset, which interpreter.reload runs on the interpreter it
  replaces before the new one imports (the old interpreter is alive during
  the reload, so the hard reset met the same cause).  The next module of
  the name takes the tables back at the same addresses, without their
  steps, and attaches its own code; the shim serves.  Found in review:
  clear wiped the built-ins the runtime registered under the name of the
  module too (the constructor alone seeded them), and a retired module
  survives while a Python object names it; under interpret:all, where no
  object re-adds the tables, the next incarnation of Control.SetFunctions
  resolved applyS to a fresh table without a step and the evaluation of
  sortValues (evalS (set f $##> 1)) dumped core after a reset (status 139;
  15 built-ins missing by name).  Module::seed_builtins copies the tables
  of g_builtin_modules in; the constructor and the end of clear call it.
  Test: TestBuiltinsAfterReset of unit_setfunctions_bugs.py, a child under
  backend:cxx,interpret:all that keeps SF.set1 across a reset and
  evaluates the goal through ($##>) and through a higher-order ($>): it
  dumped core before the seed and gives ['[1, 2]'] twice after.  Probes under
  tiered: the module route 1, 2, 3 (first.f gives 2 once second is made,
  see below); the reload route 1, 2; the import route with a data type 1,
  2, 3 and no warning.  Tests: testSameNameAfterResetWhileFirstAlive and
  testSameNameAfterReloadWhileFirstAlive of unit_compile_modules.py on
  both backends (skipped on cxx under interpret:off, where a second
  library of a live name is refused by design,
  testReusedModuleNameIsAnError); in TestSwap of unit_cxx_tiered.py,
  test_string_module_made_again_while_the_first_is_alive (the same table
  address, the new value), test_edited_module_made_again_while_the_first_is_alive
  (the first incarnation swapped before the edit; the function and the
  constructor keep their addresses; no warning; the second runs the new
  code interpreted and is swapped in turn) and
  test_edited_module_made_again_after_a_reload.  With the hook disabled,
  all five failed ([1] != [2]; the warning of the changed tables).  Left
  to the owner: (1) the second symptom of the issue: a symbol of a module
  that left the interpreter still evaluates.  Its interpreter is a weak
  reference to the same, reset, interpreter, and its table is kept; after
  the handover it runs the code of the current incarnation of its name
  when the shape agrees (first.f gives 2 once second is made, as a symbol
  kept across a reset did before when the module object was dropped) and
  its old code when the shape differs (the kept table is not taken back
  then).  A refusal needs a mark on the unlinked module or a generation
  on the interpreter, checked by curry.eval and the builders: proposed
  as an error at evaluation and at expression building, "symbol M.f
  belongs to a module that left the interpreter; import it again".  An
  API behaviour, not taken; testStaleSymbolIsRefused of
  unit_compile_modules.py (expectedFailure, cxx) pins the decision: it
  asserts that curry.eval(first.f) raises after the reset, and fails today
  because the goal evaluates to [1].  (2) Under interpret:off the import route
  after an edit still runs the first code (probe: 1, 1, 1 before and
  after the fix): the object of the second incarnation lands at the path
  of the object the process maps, and SharedCurryModule(sofile) joins the
  registry entry of the loaded library (dynload.cpp: one library per
  module name while one is loaded); _refuse_other_file passes the same
  path without a refusal or a warning, where the module route is refused
  with DynloadError.  A refusal of a changed file, or a load through a
  link of its own as the swap does, changes the loader; the issue's
  acceptance is the default interpret:tiered.
  test_edited_module_made_again_under_interpret_off of TestSwap in
  unit_cxx_tiered.py (expectedFailure) pins the decision: it switches to
  interpret:off, imports, edits, resets and imports again, and asserts
  the edited value; it fails today with 1 != 2.  (3) retire keeps the
  tables of every unlinked module, a trapped module never evaluated under
  interpret:off included: a few tables per reset.
- #118, the eager set-function strategy.  Cause: eagerApplyS_step of
  src/cyrt/currylib/setfunctions.cpp, the step of ($##>) that setN_step
  builds into the chain under the eager strategy, made the partial
  application of eagerApplyS itself and applied it with ($##): once ($##)
  had the argument in ground normal form it applied the function to it,
  which was this step again, which normalized the argument again, without
  end.  The definition of the library, ($##>) f a = (f $>) $## a, applies
  ($>), which is applyS.  So every goal with an argument looped, ground or
  not: sortValues (set1 adj binDigit) and sortValues (set1 adj 0) both
  reached the 60 s timeout (exit 124) on the C++ backend, where
  sortValues (set0 (adj binDigit)), which applies nothing, gave its four
  values.  The Python backend runs the compiled definition of the library
  and gave the values under eager.  Fix: the step builds the partial
  application of applyS_Info.  A plain defect of the step, not of the
  strategy: the eager strategy now gives the lazy values on both goals
  ([(-1), 1] and [0, 2]; [(-1), 1]) on both backends, as the dissertation's
  eager scheme does for an argument-total goal.  sortValues (set1 const1
  failed) has no value under eager on both backends (and [1] under lazy):
  the argument is normalized before the set function sees it, as the
  docstring of the flag says ("similar to PAKCS", where the goal fails
  too).  The issue reports [] for it.  That answer is not reproduced:
  the goal has no value under eager with both settings of
  setfunction_failures (encapsulate, the old default, and escape) on the
  fixed build, and the fix did not touch this goal (the loop was in the
  application of the function, which this goal never reaches); the
  reviewer saw the same on the base build.  The docstring's rule for
  'encapsulate' (a failure that drops the last alternative gives the
  empty set, rule SF.5) does not describe the eager route, where the
  argument fails before the set function starts.  Tests: TestEagerStrategy of
  unit_setfunctions_bugs.py, test_lazy_strategy and
  test_eager_strategy_ends, each a child under prlimit and timeout 120
  with the flag, on both backends; the goals adjSet and adjSet0 in
  data/curry/SetFunctionsBugs.curry.  With the C++ fix reverted and the
  runtime rebuilt, the eager test failed with status 124 after 120 s.
  Left to the owner: whether a failing argument under eager should have
  no value (today, PAKCS-like), the [] of the issue, or the lazy [1]: the
  semantics of the flag, with the wording of rule SF.5 in the docstring
  of setfunction_failures as the point to decide; func_setfunctions.py
  still skips 'free' under eager (EvaluationSuspended).
- Verification (env -i, prlimit, CCACHE_DIR set, one or two processes at
  a time).  make stage after the C++ change: 20 s, status 0 (module.hpp
  is outside the include closure of cyrt.hpp, so the ABI stamp holds);
  make overlay status 0.  Single files: unit_setfunctions_bugs.py 29 ok
  (1 skipped) on cxx and 29 ok (1 skipped, 1 expected failure) on py;
  unit_compile_modules.py 5 ok on cxx and 5 ok (1 skipped) on py;
  unit_loadsave.py 10 ok (4 expected failures) on cxx;
  unit_cxx_tiered.py 33 ok on cxx (51 s).  The new tests verbosely on cxx:
  each ok, none skipped.  Through the runner (./run_tests -j 2 --mem 4
  --backend both) over unit_loadsave, unit_compile_modules,
  unit_cxx_tiered, unit_setfunctions_bugs, unit_setfunctions_semantics,
  unit_cxx_interp, unit_inspect, unit_api, unit_cxx_checker,
  unit_py_checker, unit_cxx_emit, unit_importer, unit_cxx_bom,
  unit_prelude, unit_compile, unit_goals and func_setfunctions: 34 of 34
  run, 2 failed, wall 121.6 s.  The two, unit_cxx_bom.py (4 errors,
  KeyError 'cxx.shlib') and unit_inspect.py (2 failures, the bytecode of
  Peano.add) on cxx, read the compiled objects of the corpus, which the
  fresh worktree lacked (no CxxBom.so or Peano.so beside the sources);
  the prepare pass (./run_tests --prepare-only --backend cxx: 12 of 12,
  444 s) compiled them, and the two files then passed through the runner
  (unit_inspect.py 27 ok, unit_cxx_bom.py 10 ok on cxx).  The whole suite
  was not run (longer than the foreground bound of this lane).
- Review pass (same day).  Applied: the seed of the built-ins after clear
  (above); the record of set1 const1 failed under eager (above); the two
  expectedFailure tests of the owner's decisions on #114 (above);
  TestEagerStrategy moved above the __main__ block of
  unit_setfunctions_bugs.py, where a direct run of the file skipped it;
  the blank lines inside two docstrings (tiered.py, the unit file) and
  the doubled blank lines in module.hpp, module.cpp, interface.py,
  backends/__init__.py, interpreter/__init__.py and loadsave.py removed.
  Not taken: a lookup of get_builtin_symbol in Materializer.symbol; the
  seed makes get_infotable find every built-in again.

2026-10-10: pothole batch 4 (the products lock, the swap of imports, new-worktree.sh offline, the .interp line, the program's directory, the depth bounds, the rename of the binding guard; worktree wt/potholes4)
- (1) The lock of the products of a module.  Two compiles of one module
  into one product directory raced: the background child of the tiered
  compile and a foreground sprite-make --so (the sequence of the
  Quickstart), two sessions over one tree, or two test processes that
  import one stale module; both wrote M.cpp and M.so in place, one could
  read a torn file, and test_quickstart_sequence_twice waited for the
  background compile while the Quickstart told the reader to wait.  Fix:
  curry.toolchain._productlock, an advisory lock (flock) on <Module>.lock
  in the product directory, held by _makecurry.Maker.make from the first
  step that must run to the end of the plan, and by the compile on first
  use (materialize.compile_pending).  The loser waits (one INFO line names
  the lock); once it holds the lock it computes the current file again,
  so when the winner wrote a current object it makes nothing.  The lock
  dies with its process, the empty lock file may stay, a product directory
  that cannot be made or written gets no lock (the steps report that), the
  lock is per module (sprite-make --jobs compiles different modules at
  once as before; the locks of the imports are taken in the order of the
  import graph, which is acyclic) and re-entrant within a process.  The
  Quickstart test runs sprite-make while the background compile may run;
  the Quickstart and the sprite-make page say so.  Tests: TestProductLock
  of unit_make.py (the lock file of every file of a chain, the free lock
  and its re-entrance, a wait for a child that holds the lock, no lock in
  an unwritable directory, and two sprite-make --so of one module started
  while the test holds the lock: both exit 0, one "Compiling" line across
  the two logs, the object and its stamp current, the object loads and
  runs).  The whole-file race was never deterministic, so the two-compile
  test holds the lock itself while the children start; the wait of the
  loser and the single compile are the observed facts.  Review: a file
  system that refuses flock (ENOLCK without a lock service, ENOSYS,
  EINVAL) ended the compile with the OSError at its first step; now the
  OSError of either flock gives no lock, as an unwritable directory does,
  and the compile runs unlocked (test_no_lock_without_flock: flock
  patched to raise ENOLCK, the JSON step of a plan runs).
- (2) The swap of the imports.  Under tiered a job carried only its own
  object; an importer whose import had not swapped (its job later in the
  queue, or none) failed its dlopen with the message of the dynamic linker
  (the SONAME of the import unmapped) and stayed interpreted.  Fix:
  TieredJob.imports (tiered.hpp), the objects of the imports the
  interpreter runs, direct and indirect, in dependency order, each with
  the ICurry digest of its import; apply (tiered.cpp) maps each one whose
  module has no object before the object of the importer (map_import),
  under the same check of the ICurry (an edited import refuses the swap,
  as its own job does), and adopts it into the module of the import with
  no step to count; a job whose module has an object already (mapped by
  an importer's swap) adopts its steps from it.  The Python side records
  every module the interpreter runs at its load (tiered._record: the
  object its compile writes, a private copy's under a foreign budget, the
  ICurry file and its digest), builds the list at submit
  (_import_objects; a module loaded from its object ends the walk), and
  poll refreshes the record of a mapped import (the result field mapped;
  one INFO line).  A module of type synonyms alone has no table, no shim
  and no job, and its importers name its object: it is on record too.
  Tests: TestImportObjects of unit_cxx_tiered.py (an importer of such a
  module swaps and the import gets its object on record; the control,
  with _import_objects stubbed to nothing, fails with "cannot open shared
  object file" as before; the job of an import run after the job of its
  importer, both swapped, the import mapped first and its job adopting the
  mapped object).  A header change (tiered.hpp is in the closure of
  cyrt.hpp), so the ABI stamp changed and the library and the pool were
  compiled again (make stage, the prepare pass).  Review: poll refreshed
  the records of the mapped imports only after a swap of the importer,
  though apply maps them before it loads the object of the importer; when
  that load failed, the import ran compiled with no object on record, and
  a later load of another importer of it refused the object.  Now poll
  refreshes every mapped import whatever the result of the importer
  (test_import_mapped_before_a_failed_load: the object of the importer
  cut short after the child wrote it, before the poll).
- (3) scripts/new-worktree.sh offline.  The submodule step cloned pybind11
  from its URL, which a sandbox without a network, or a clean environment
  without the proxy variables, cannot reach.  Now the step clones from the
  checkout of the main tree ($here/extern/pybind11) with the URL of the
  submodule overridden for the one command (-c submodule.<name>.url=...)
  and the file transport allowed (-c protocol.file.allow=always; git
  refuses it for a submodule since 2.38.1), then sets the remote of the
  clone to the URL of .gitmodules in the configuration of the clone alone
  (submodule sync would write the shared configuration of the repository,
  which this sandbox masks: "could not lock config file");
  when the main tree has no checkout or lacks the commit, the plain update
  fetches from the URL as before.  The header names the step as the
  exception to the clean environment.  Verified in a scratch worktree
  (git worktree add -b wt/potholes4-check2 .../wt-check2) under env -i
  with no proxy variable: the clone, the checkout of 97bf890d, the remote
  https://github.com/pybind/pybind11, git status clean; the worktree and
  the branch were removed afterwards.  In that session git worktree
  remove failed to delete .git/worktrees/<name>, so git worktree prune
  was run by hand, once, outside the sandbox; it also pruned three stale
  entries of earlier lanes (bugs3, conda2, cxxcheck: their gitdir files
  no longer existed).  The review's session removed such a worktree
  inside its sandbox without a failure, so the cause was not established.
- (4) The .interp line of curry.save.  _generate_main of the C++ emitter
  wrote a .interp section with config.ld_interpreter_path into the text of
  a program (curry.save with a goal); no shipped or compiled object
  carried it and nothing read it.  The lines are gone (the compiler
  imports config no more); curry.load reads a shared object, never the
  text, so nothing tolerated or parsed the line.  conda/README.md question
  13 says the section is gone and that the sysconfig value stays, unused
  by the code (dropping the value, its file and test_ld_interpreter_path
  is the owner's).  Test: TestSavedProgram of unit_loadsave.py (the text
  of a program saved with a goal has no .interp, my_interp or ld-linux;
  on the C++ backend the program compiles with Cpp2So and loads after a
  reset, main gives 42; on the Python backend the saved script prints
  42).
- (5) The program's directory on the search path.  sprite-make --so
  Second.curry beside Smoke.curry failed with "Curry module 'Smoke' not
  found" unless CURRYPATH named the directory: the front end found Smoke
  (PAKCS searches the directory of the program first), the plan's own
  import of the ICurry (Json2TargetSource) and the imports of the module
  searched interp.path.  Fix: _findcurry.program_path, the directory of a
  program given as a file first, then the path; makecurry and currentfile
  use it (prune_stale and the is_stale of the steps take the path, so
  Cpp2So judges an object by importing its imports on it, and the fake
  steps of unit_findcurry.py and unit_plan.py take the argument); the
  steps import on it (Json2TargetSource, Cpp2So._dependencies,
  import_lacks_an_object, restore); ImportEx prepends it for a source
  file and imports the imports of such a module on import_path, the path
  of the program followed by the entries of interp.path it lacks (so the
  siblings of a program are found, and a library module as before),
  while a module given by name, with or without a currypath, keeps the
  search of its imports on interp.path, as before: the path given with a
  name finds that module alone.  The review confined it so (the first
  version searched the imports of a named import on the given path too,
  a wider meaning of the currypath argument of curry.import_, and the
  steps of the plan did the same for a named plan):
  _findcurry.imports_path(currypath, is_sourcefile) is the search path
  of the imports as the steps import them, the program's path for a
  source file and None (interp.path) for a name; currentfile hands it to
  prune_stale, Json2TargetSource, Cpp2So and its restore derive it from
  the is_sourcefile keyword of the plan (Plan.restore passes the
  keywords), the first-use compile of the C++ backend passes the path it
  recorded at module_loaded as the imports' path, and module_loaded gets
  the search path of the imports (the program's path, or interp.path for
  a name; test_import_by_name_with_a_path, test_import_of_a_path under
  interpret:off too); the job
  graph of sprite-make --jobs visits the imports on it; the loader
  imports the modules an object needs from beside its source first.  The
  EnvironmentVariables page, the sprite-exec page and the sprite-make
  docstring say so.  Tests: test_program_path of unit_findcurry.py;
  TestProgramDirectory of unit_make.py (two modules in a directory no
  CURRYPATH names, the tools run from elsewhere: sprite-exec prints 8;
  sprite-make --so writes both objects, sprite-exec runs them, a second
  sprite-make --so leaves the object, curry.load of the object imports
  the import; curry.import_ of the path in a child).
- (6) The depth bounds past #125.  Made iterative: icurry.visit (an
  explicit stack; the order of the visitor calls is the recursive one,
  post-order or top-down, and a top-down replacement is walked into);
  copy.deepcopy of an ICall or an IOr (__deepcopy__, the nested calls and
  choices on a stack under the memo of copy.deepcopy, so the sharing and
  the metadata are copied as before; every copy site of the inliner and
  of partials gains); the walks of the inliner over an expression
  (node_count, _valid_expr, _count_uses, _symbols, _function_symbols,
  _named_functions, _max_vid over one _subexpressions generator;
  _substitute in place on a stack; Inliner.expr on the trampoline, the
  rewrite of a result a walk of its own at most MAX_DEPTH deep); the
  rewrite of saturate_applies (the trampoline); and compileE of the
  generic compiler (the trampoline: a leaf answers its text, a node is a
  generator, compileArgs counts the nesting around the yields, so the
  hoisting of the Python backend is unchanged and the symbol imports keep
  their order).  utility.trampoline takes a value in place of a generator
  as a result (and a value in place of a walk), so a dispatch may mix the
  two.  HOIST_DEPTH is a module constant of backends.py.compiler with its
  sentence.  Measured after the change: a literal list of 10800 elements
  imports and runs on both backends (8.8 s on cxx under interpret:new,
  177 MB); the chain of nested if-then-else costs four frames per level
  in the monadic analysis (506 levels under a limit of 2048), about 4000
  under maxrecursion (680 before; the visitor's frames are gone), and is
  bounded by time first: the case lifting of the port is quadratic in the
  chain (74 s of the 118 s of the import at 2000 levels, under cProfile;
  3500 levels import in 255 s); the ICurry JSON codec recurses in C and
  overflows the 8 MB stack between 12000 and 15000 levels of nesting
  (both measured, dumps then loads of a nest of calls).  Documented in
  the developer notes ("The depth bounds of the toolchain").  Tests:
  TestDeepExpressions of unit_curry2icurry.py at 5400 (the copy, twice
  2700), 8000 (compileE, twice 4000) and 10800 (the visitors, twice
  5400), each a generated module imported and evaluated on the backend of
  the run under interpret:new; TestVisit and TestDeepCopy of
  unit_icurry.py (the order against a recursive reference walk, the
  module walk, the replacement followed, a nest of 20000 at a recursion
  limit of 1000; the copy's identity, sharing and metadata, a nest of
  20000); TestTrampoline of unit_utility.py.  With the walks reverted the
  import of 2800 elements failed in copy.deepcopy and the chain of 700 in
  set_monadic_metadata (the probes before the change).  The digest of the
  generator sources changed (compileE, visit, inlining, iexpression), so
  the product cache misses once per module.
- (7) The rename and the budget.  The table of the &> of the runtime is
  CyI7Prelude12BindingGuard in the sources (prelude.hpp declares it
  without a define, with the history of #124 beside it; constr.cpp
  defines it with the step BindingGuard_step; rts_bindings.cpp names it);
  seq_Info and seq_step are gone.  The propagation budget of the checkers
  keeps its one name per checker (CHECKER_PROPAGATE_BUDGET,
  PROPAGATE_BUDGET, 1024) and gets one comment each: one budget for the
  walk of a step, a pull-tab, a copy and a generator by policy, a larger
  budget for the steps alone or one by the size of the result the
  owner's.
- Verification (the clean environment, CCACHE_DIR set, prlimit, at most
  two test processes at a time).  make stage after the C++ changes: the
  first run stopped in the prebuild of the library on icurry_is_stale,
  which took one argument (the fake steps of two test files took one as
  well; both fixed), the second ran to status 0, and again status 0 after
  the last change of tiered.cpp; make overlay status 0 each time; the
  prepare pass of the runner on cxx: 12 of 12 run, 0 failed, wall 465.7
  s (the ABI stamp changed).  Probes before the change (interpret:new on
  cxx): a list of 2800 elements failed at import with RecursionError in
  copy.deepcopy (8181 frames of 16384), a chain of 700 nested
  if-then-else in set_monadic_metadata (4778 frames of visit, 682 of
  _checkmonadic); after it: 2800, 5400, 8000 and 10800 elements import
  and give [1, 2] (2.0 s, 3.9 s, 5.9 s and 8.8 s), the chains of 1400,
  2500, 3000 and 3500 import (the last two in 162 s and 255 s; 4000 did
  not end within 300 s, its front end alone took 75 s); 10800 elements
  on the Python backend too.  Single files, each ok: unit_optimize_inline
  (31), unit_optimize (18), unit_optimize_applies (25), unit_icurry (13),
  unit_cxx_emit (5, 2 skipped), unit_utility (14), unit_findcurry (10),
  unit_plan (9), unit_loadsave (10, 4 expected failures on cxx, 4 skipped
  on py), unit_make (26, 1 skipped on cxx, 3 on py), unit_cxx_tiered (33,
  52 s), unit_cxx_runtime (9), unit_setfunctions_bugs (27, 1 skipped),
  unit_curry2icurry (21, 48 s on cxx, 50 s on py), unit_py_checker (46 on
  py), unit_cxx_toolchain (49).  Through the runner (./run_tests -j 2
  --mem 4 --backend both): unit_cxx_tiered, unit_make, unit_loadsave,
  unit_cxx_toolchain, unit_cxx_interp, unit_reload, unit_prebuild,
  unit_product_cache, unit_cxx_runtime, unit_setfunctions_bugs,
  unit_conda, unit_api, unit_goals, unit_cxx_steplimit, unit_findcurry,
  unit_icurry: 32 of 32 run, 0 failed, wall 142.1 s (a first run found
  the two failures of unit_cxx_toolchain: the lock file in the listing of
  the product directory, now expected, and the Prelude not found by
  import_lacks_an_object in a child whose import gave a partial path,
  which _findcurry.import_path settles); unit_curry2icurry, unit_icurry,
  unit_utility, unit_findcurry, unit_plan, unit_optimize,
  unit_optimize_inline, unit_optimize_applies, unit_cxx_emit,
  unit_cxx_checker, unit_py_checker, unit_cxx_bom, unit_cxx_partial,
  unit_cxx_passthrough, unit_flat2icurry, unit_parse, unit_py_interp,
  unit_cxx_heap: 36 of 36 run, 0 failed, wall 134.6 s.  make -C docs
  html: build succeeded.  Not run: the oracle suites (func_*.py) and the
  whole suite on both backends, the suite stage.  The review's runs after
  its changes (the three new tests fail with the change of each toggled
  off): unit_make, unit_cxx_tiered, unit_cxx_toolchain, unit_findcurry,
  unit_api, unit_product_cache, unit_prebuild, unit_loadsave, unit_reload,
  unit_cxx_interp, unit_plan, unit_cxx_runtime through the runner on both
  backends: 24 of 24 run, 0 failed, wall 103.6 s.
- Left to the owner.  (1) The sysconfig value ld_interpreter_path, its
  file and test_ld_interpreter_path are unused by the code now (item 4).
  (2) The case lifting of the port is quadratic in a chain of nested
  if-then-else (74 s of 118 s at 2000 levels), and the monadic analysis
  recurses per level of such a chain (about 4000 levels); the ICurry JSON
  codec recurses in C (12000 levels pass, 15000 overflow the 8 MB stack).
  (3) TieredJob::imports and TieredResult::mapped live in tiered.hpp,
  which is in the include closure of cyrt.hpp (prelude.hpp, checker.hpp
  and module.hpp are not), so the ABI stamp moved and every installation
  that takes this commit compiles its compiled Curry products again,
  once (the prepare pass here: 465.7 s); keeping the list in the state of
  tiered.cpp would need a declaration in some header all the same.
  (4) tests/manifest.json has no entry for the new tests (a calibration
  run adds them).  (5) The lock file <Module>.lock stays in the product
  directory after a compile; the listing test of the stamp expects it.

2026-10-10: the eager strategy and a failing argument (closed)
- The owner asked whether sortValues (set1 const1 failed) should give [1].
  Under the default strategy (lazy) it does, on both backends and under
  both failure rules: const1 does not demand its argument, so the failure
  is never demanded (probed on 8f2315f9).  Under setfunction_strategy:eager
  the argument is reduced to ground normal form before the set function
  sees it, by the flag's definition, so a failing argument gives no value,
  on both backends and under both rules, as the strict set-functions
  package of PAKCS does; sortValues (set1 const1 (1 ? failed)) gives [1]
  under eager, the choice in the argument being outside the set.  The []
  of the report of #118 came from the looping step that the batch of
  2026-10-10 fixed.  Closed: no value under eager, by definition; the open
  item of that batch's entry is withdrawn.

2026-10-11: the eager set-function strategy dropped (worktree wt/eager)
- The decision.  The owner decided on 2026-10-11 that the eager strategy
  of the set functions goes.  The strategy was implementation only: the
  interpreter flag setfunction_strategy with the values lazy and eager,
  from the code base of 2021.  Nothing in the theory, the harness or the
  documentation of the semantics depends on it, and the lazy strategy is
  the only one the Fair Scheme proofs describe.  The lazy strategy does
  not change: every existing test of the set functions keeps its values.
- Files changed, and the symbols removed.  src/python/interpreter/flags.py:
  the entry of setfunction_strategy in the docstring and in FLAG_INFO.
  src/cyrt/fwd.hpp: enum SetFStrategy (SETF_EAGER, SETF_LAZY).
  src/cyrt/state/rts.hpp and rts.cpp: the constructor argument and the
  field setfunction_strategy of RuntimeState.
  src/python/backends/cxx/cyrtbindings/constants.cpp: the Python enum
  SetFStrategy; graph.cpp: the argument of both constructors of
  RuntimeStateBase (the comment on the positions of the rotation cadence
  moved by one); __init__.py: _SETF_STRATEGY and the attribute
  setfunction_strategy of RuntimeState.  src/cyrt/currylib/setfunctions.cpp:
  eagerApplyS_step, the table eagerApplyS_Info (named "$##>"), its entry
  "eagerApplyS" in the built-in symbols of Control.SetFunctions, and the
  branch of setN_step that chose it (the chain is built with applyS_Info
  alone); the include of prelude.hpp went with applygnf_Info, its last
  use there.  setfunctions.hpp: the define and the extern of
  eagerApplyS_Info.  src/python/backends/py/eval/rts.py: the attribute
  setfunction_strategy.  src/python/backends/py/currylib/setfunctions.py:
  setN builds the chain with ($>) alone.
  docs/source/PythonAPI/CurryPackage.rst: the row of the flags table.
  docs/source/CurryPrimer/Semantics.rst: the paragraph on the lazy
  evaluation of a set function names no flag.  The C++ symbol names of
  the lazy path are unchanged.  The ($##>) of the library
  (curry/lib/Control/SetFunctions.curry) stays and runs from its
  definition on both backends, as before: a built-in is looked up by its
  ICurry name, and no function of the library is named eagerApplyS, so
  the removed table was reached through setN_step alone.
- Tests moved.  TestEagerStrategy of unit_setfunctions_bugs.py is
  TestFirstSetFunction, with the lazy test alone: test_lazy_strategy
  evaluates adjSet and adjSet0 of data/curry/SetFunctionsBugs.curry in a
  child without the flag; test_eager_strategy_ends is gone.  The skip of
  'free' under eager in func_setfunctions.py is gone, and with it the
  imports of the file that nothing used (curry, logging, curry.config,
  capture_log; the skip was the last use of curry): the file imports
  cytest alone, 39 tests on each backend as before.  The two direct
  constructions of RuntimeStateBase in unit_cxx_rotation.py
  (test_zero_quantum_is_an_error) lost the argument.  New:
  test_setfunction_strategy_is_unknown of TestFreshInterpreterFlags in
  unit_api.py, beside test_unknown_flag: the flag is not in curry.flags,
  and Interpreter(flags={'setfunction_strategy': 'eager'}) raises
  ValueError "unknown flag: 'setfunction_strategy'".  The docstring of
  the unit file and the comments of the Curry module keep the history of
  #118.
- A program that sets the flag now sees the error of an unknown flag.
  Interpreter(flags={'setfunction_strategy': ...}) raises ValueError
  "unknown flag: 'setfunction_strategy'" (Interpreter.__new__).  In
  SPRITE_INTERPRETER_FLAGS (setfunction_strategy:eager), and in
  curry.reload({'setfunction_strategy': ...}), which passes its flags
  through that variable, the flag is dropped with the log line
  "[WARNING] unknown flag: 'setfunction_strategy'" and the interpreter
  starts without it; observed on both routes, and the same as for
  'bogus'.
- Verification (env -i, prlimit, CCACHE_DIR set, at most two test
  processes at a time).  make stage after the C++ changes: status 0 in
  47 s (the warning on module.cpp:144, 'found' set but not used, is
  older); make overlay status 0 in 9 s.  Single file:
  unit_setfunctions_bugs.py on cxx, 29 tests ok (1 skipped) in 12 s; the
  worktree had no ICurry cache and no products of SetFunctionsBugs, so
  the run sent the module through the front end, inside the sandbox,
  without trouble.  Through the runner (./run_tests -j 2 --mem 4
  --backend both): unit_setfunctions_bugs (29 tests on each backend; 30
  before), unit_setfunctions_semantics (43), unit_setfunctions_bindings
  (16), func_setfunctions (39), unit_api (13; 12 before): 10 of 10 run,
  0 failed, wall 32.0 s; unit_py_checker (46), unit_cxx_checker (57),
  unit_cxx_runtime (10), unit_cxx_rotation (21): 8 of 8 run, 0 failed,
  wall 43.5 s.  The new test and test_lazy_strategy by name (-v) on both
  backends: ok.  make -C docs html: build succeeded in 17 s, and no page
  of the output names the flag (the two docutils errors on the target
  "interpreter.import", in docstrings of backends/__init__.py and
  cxx/interface.py, are older).  Not run: the whole suite on both
  backends (the suite stage).
- Notes for the owner.  (1) fwd.hpp and rts.hpp are in the include
  closure of cyrt.hpp, so the ABI stamp moved: every installation that
  takes this commit compiles its compiled Curry products again, once
  (make stage compiled the library here; the prepare pass of the runner
  does the test pool).  (2) tests/manifest.json counts 30 tests for
  unit_setfunctions_bugs.py and 12 for unit_api.py; the files have 29
  and 13 now (a calibration run updates it).

2026-10-11: five bugs of the audit of the implementation against the proved rules of the Fair Scheme (#126 to #130; worktree wt/bugs9)
- The source.  The audit of 2026-10-11 compared both backends with the
  proved rules of the Fair Scheme and its extensions (the base scheme,
  FS-x, FS-beta, FS-S) and filed five correctness bugs with reproducers,
  the values of KiCS2 and the rule each one breaks.  Each fix is at the
  cause, and each gets a unit test on both backends; the checker mode of
  both backends gains the check the issue names, where it fits.
- (1) #126, the C++ scheduler at rule N.1.  procN (fairscheme.cpp), case
  T_FAIL, returned root->make_failure(), which forwards the root cell of
  the configuration to a failure in place.  After a fork the root is a
  cell a sibling configuration shares: let p = (failed, 1) in p ? (snd
  p, 2) gave no value (the Python backend and KiCS2: (1, 2)).  Rule N.1
  returns false and rule D.5 drops the configuration; the discards change
  no cell (Def 2.40), and a replacement by failure is a step at an
  operation cell alone (Def 2.24).  Fix: procN returns T_FAIL when its
  root is the root of the configuration (root == C->root), and procD
  drops the configuration; the other caller, applynf_step, scans from its
  redex, an operation cell, which becomes a failure as before (the Python
  N: if var.is_root: rts.drop(), else the redex is rewritten).  The other
  make_failure sites (basic.cpp, read.cpp, icurry.cpp) write the redex of
  a step.  Tests: TestFailedSuccessorOfSharedRoot of unit_cxx_scheduler.py
  (failedSiblingPair, failedSiblingJust, failedSiblingFirst of
  data/curry/CxxScheduler.curry; both backends; once plain and once under
  the checker).  Checker (both backends): Def 2.40 (discard).  At the
  dispatch of a configuration the root cell is recorded when it is a
  constructor, a set guard or a constraint cell (an operation cell at the
  root is rewritten in place by its step); at a drop (procD T_FAIL; D and
  N at the root) the cell holds the same symbol and the same successors,
  read through forward cells now for both records, since a step below the
  root forwards its operation cell and the compression of a chain writes
  the slot to the end of the chain.  The check was not run against the
  old runtime (no build pairs the old procN with the new checker); the
  review pins it on a constructed state instead
  (test_discard_changes_no_cell of unit_py_checker.py: a changed
  successor, and a root forwarded to a failure, each reported).  The
  record of the dispatch is one per scheduler (dispatch_stack of both
  checkers): a set function evaluated inside a step runs a nested
  scheduler on the same checker, whose dispatches and drops overwrote the
  one record, so the outer drop compared the wrong root or skipped the
  check (allValues_step and allValues push a record around their procD
  and D: enter_scheduler, leave_scheduler;
  test_discard_record_per_scheduler).
- (2) #130, the Python D at a set guard of an enclosing set.  D, case
  T_SETGRD, called rts.unwind() for a guard whose set is not the current
  set, and allValues then asserted on the tag of the root of the nested
  configuration: capNested and nestFail of SetFunctionsSemantics raised
  an internal error on the Python backend ([[1]], [[2]] and no value on
  the C++ backend, whose procD lets T_SETGRD fall to procN).  A monitor is
  bookkeeping: the box dereference passes through it at any position, the
  root included (Def 2.77).  Fix: D strips a guard of the current set
  (escape_all, as before) or a guard outside every set function, and
  hands a guard of an enclosing set to N, which descends into it like a
  constructor; a choice below it is pulled up through it into the escape
  set of its set and escapes, and the value keeps the guard.  Tests: the
  known failures are gone: PY_KNOWN_FAILURES of
  unit_setfunctions_semantics.py and of unit_py_checker.py (capNested and
  nestFail run on both backends, under the checker too),
  test_nested_set_function of unit_setfunctions_bugs.py runs on both
  backends (test_nested_set_function_python is gone), TestNestedSetGuard
  of unit_cxx_scheduler.py runs on both backends (TestNestedSetGuardPython
  is gone).
- (3) #127, the generator of a decided class at the union-find root.
  get_generator read the generator at the root of the class alone
  (rts_freevars.cpp, rts_freevars.py), and the union by size can make a
  variable without a generator the root: in g16b the class {a, b}, b
  decided, is united into {c, d, e} by a =:= c, c becomes the root, and
  the yield of c copied the Unit marker of the variable into the value
  (C++: (True, True, ())) or asserted (Python).  Def 2.74 and Thm 3.28:
  c is bound through the decided class and is replaced by its generator.
  Fix at the union: _constrain_equal_rec (rts_constraints.cpp,
  rts_constraints.py) finds its pivot among the two variables of the
  constraint, the root, or any member of the class (the ids united; the
  parent table in Python), and gives the root and the two variables a
  clone of the pivot's generator when they have none, paired leaf by leaf
  (pair_generators), so a decided class has a generator at its root.
  get_generator calls _constrain_equal_rec for a variable without a
  generator (the former constrain_equal(x, root) did nothing when x was
  the root) and throws a logic_error (C++) or asserts (Python) when the
  class has none; the same path serves apply_binding at a fork, which
  reads the generator of the root before the fork's constrain_equal runs.
  Tests: TestDecidedClassRoot of unit_constraints.py (g14, g16, g16b of
  data/curry/Constraints.curry, the values of KiCS2, both backends).
  Checker (both backends): E5 (group generator), in the fingerprint
  check at every fork, constraint step and yield: a decided class of two
  or more variables has a generator at its root (the C++ check reads the
  ids united, the Python check a decided member other than the root; a
  decided singleton without a generator is a constructed state of
  test_no_decided_variable_in_a_value).  The check was seen to fire on a
  constructed state alone (test_group_generator of unit_py_checker.py);
  the review ran the Python backend with the three rts_*.py files of the
  fix reverted under the new checker on g16b and saw the right values
  with no E5 report, so the check is not shown to catch the old defect
  on g16b on the Python backend (the Def 2.62 check was seen to fire on
  g20 and g15 with constr.py reverted).
- (4) #128, the merge of two bound classes.  At a union, update_binding
  moved the binding of a constraint id to the new root and dropped the
  Boolean of add_binding when the root held a different binding (the C++
  assertion was commented out; the Python add_binding asserted on a
  binding of a non-builtin type): g18 gave (A, A) and (B True, B True) on
  the C++ backend, a value that violates the program's own constraint
  inner y =:<= False, and an internal error on the Python backend (KiCS2:
  (A, A)).  Two classes bound to different terms have no common instance,
  so the step fails.  Fix: the bindings of a class sit under its root
  (add_binding looks the class up by its root; the former C++ lookup by
  the id missed a binding of the group under another key and overwrote
  it); constrain_equal moves the bindings of the two roots before the
  union to the root after it (merge_binding in rts_constraints.cpp;
  update_binding in rts_bindings.py, which returns the Boolean now) and
  fails the step when the roots held different bindings; the old key is
  erased.  The former C++ update_binding moved the binding of the
  constraint's id alone, so the binding of a class whose root was neither
  id of the constraint stayed under the old root, a non-root key no reader
  looks up (orphan of Constraints.curry gives (True, True) now; the old
  build was not probed on it).  Two bindings are the same term when they
  are one cell, or the same constructor or literal over the same
  successors through forward cells, within a budget of 1024 cells
  (same_binding, _same_binding); two distinct cells that are not
  constructor terms count as different, and so does a pair beyond the
  budget: the step fails.  Tests: TestMergeOfBoundClasses of
  unit_constraints.py (g5 no value, g18 (A, A), g18t False, eqJust the
  bindings to equal terms in distinct cells kept, orphan; both backends).
  Checker (both backends): E11 (one binding per class), at every fork,
  constraint step and yield: every key of the bindings is the root of
  its class (test_one_binding_per_class of unit_py_checker.py, on a
  constructed state).  The constraint step at the root is a new event of
  both checkers (constrained), with the fingerprint check as at a fork.
  same_binding compares an unboxed pointer successor (format 'x': the
  head of a partial application, the set of a set guard) by its value,
  as graph/equality.cpp does; the first version returned false for it,
  where _same_binding of the Python backend compares such a successor
  with !=.  The open case of the merge has a test: eqShapeG18 of
  Constraints.curry, let e = x =:= y in (e &> (0, 0)) ? ((x =:<= (1 +
  1)) &> (y =:<= (1 + 1)) &> e &> (x, y)), gives (0, 0) alone on both
  backends where KiCS2 gives (0, 0) and (2, 2): the shared constraint
  cell e unites the two classes while their bindings are two unevaluated
  cells, and the step fails.  Without the shared cell the step copies
  the bindings in first and the value survives.
  TestMergeOfEqualUnevaluatedTerms of unit_constraints.py expects both
  values under expectedFailure, so the owner's decision turns it green.
- (5) #129, the equality steps and a generator of another configuration.
  The steps read "bound" as "has a generator" before the fingerprint:
  hnf, case T_FREE, called replace_freevar (rts_freevars.cpp) and the
  Python hnf did the same, which writes the slot whenever the variable
  has a generator.  A variable narrowed by another alternative was
  narrowed again in a configuration whose fingerprint holds no decision
  for it; under =:<= the right side was then evaluated in each
  alternative: g20 lost (True, True), and g15 yielded four ground values
  where the theory and KiCS2 yield (x, x) once.  Def 2.57: a variable is
  bound in a configuration when the fingerprint decides its group or a
  binding holds it; Def 2.62 and Def 2.75 case 2: the equality steps bind
  a free variable by a constraint cell or a non-strict binding, without a
  step at the right side.  Fix: the equality steps alone ask hnf_or_free
  for a variable that is free in the configuration: constrEq_step and
  nonstrictEq_step pass the marker FREE_IN_CONFIGURATION (a value set of
  kind 'v' and no values, constr.cpp), which replace_freevar reads: with
  it a variable is narrowed when the fingerprint decides its group and no
  binding holds it (its generator, or a clone through the class), a
  binding is copied in as before, and otherwise the variable is reported
  free (E_RESIDUAL, which hnf_or_free takes back); the Python hnf has the
  keyword free, hnf_or_free passes it, and constr.py sets it.  The other
  callers of hnf_or_free and hnf (($!), ensureNotFree, the primitives,
  the case of a step) narrow a variable with a generator as before:
  strictApp of Constraints.curry gives the same four values on both
  backends (the first version of the fix put the rule into hnf_or_free
  itself, and ($!) of the Python backend then yielded _a where the C++
  backend narrowed).  The binding store keeps its design.  Tests:
  TestEqualityOfFreeVariable of unit_constraints.py (g20 the three values
  of KiCS2; g22 unchanged; g15 (_a, _a), (True, False), (False, True), the
  yield form of KiCS2 with the same instance set; strictApp; both
  backends).  Checker (both backends): Def 2.62 (equality step), at an
  instantiation and at a slot write inside a step of constrEq or
  nonstrictEq: the step narrows a variable whose group is undecided and
  unbound against a constructor on the other side alone; two free
  variables make a constraint cell, and a free left side of =:<= takes
  the right side as a binding (the first version flagged every narrowing
  of such a variable and fired on g14, where a =:= c narrows a against the
  constructor c was bound to).  The Python hnf gained the hook slot_write
  for the write of an existing generator, which had none.
- Documentation.  The module docstring of checker.py and the checker
  section of DeveloperNotes.rst list the new events and checks (the
  constraint step, the dispatch and the drop, the slot write).
- Verification (the clean environment, CCACHE_DIR set, prlimit, at most
  two test processes at a time).  The reproducers of the five issues,
  re-run from the issue text under sprite-exec with steps:65536 before the
  change: goalA, goalB no value on cxx; g16, g16b (True, True, ()) on cxx
  and an internal error on py; g18 (A, A) and (B True, B True) on cxx,
  g18t False and True, both an internal error on py after the first
  value; g20 two values on both; capNested and nestFail an internal error
  on py.  After the change, on both backends: goalA (1, 2), goalB (Just
  1, 2), goalC (1, 2); g14, g16, g16b the values of KiCS2; g5 no value,
  g18 (A, A), g18t False; g20 (True, True), (True, False), (False, True),
  g22 (True, True), g15 (_a, _a), (True, False), (False, True); capNested
  [[1]], [[2]], nestFail no value; eqJust (Just True, Just True), orphan
  (True, True), strictApp False, False, True, False, bindRoot and
  bindRoot2 (False, True), goalE ((1, 2), 0), g7a, g7b and g17 the values
  of the audit; g20 and g15 under checker:true on both backends: the same
  values, no violation.  make stage after the C++ changes: status 0 (four
  runs; one stopped on a compile error of the checker, fixed); make
  overlay status 0.  Single files, each ok: unit_cxx_scheduler.py (10 on
  cxx, 10 on py with 2 skipped), unit_constraints.py (13 on each
  backend), unit_setfunctions_semantics.py (43 on py),
  unit_setfunctions_bugs.py (28 on py, 1 skipped), unit_py_checker.py (46
  on py), unit_cxx_checker.py (57 on cxx).  Through the runner
  (./run_tests -j 2 --mem 4 --backend both): unit_cxx_freevars,
  unit_free_markers, unit_funpat_free_argument, unit_funcpat_narrowed,
  unit_py_residuation, unit_setfunctions_bindings,
  unit_setfunctions_semantics, unit_setfunctions_bugs,
  unit_cxx_scheduler, unit_constraints, unit_cxx_checker,
  unit_py_checker, unit_cxx_runtime, unit_equality, unit_py_constraints,
  unit_fingerprint, unit_goals: 34 of 34 run, 0 failed, wall 99.1 s;
  unit_py_evaluation, unit_py_pulltab, unit_py_fairness, unit_cxx_interp,
  unit_cxx_partial, unit_cxx_passthrough, unit_cxx_rewrite,
  unit_cxx_rotation, unit_cxx_gc, unit_cxx_stack, unit_cxx_steplimit,
  unit_prelude, unit_currylib, unit_api, unit_expr, unit_typed_expr,
  unit_int_semantics, unit_long_lists, unit_show, unit_py_runtime,
  unit_cxx_variable, unit_cxx_heap, unit_py_copy, unit_py_graph,
  unit_py_control, unit_io_bind, unit_loadsave, unit_reload,
  unit_examples, unit_cxx_counters, unit_cxx_tiered: 62 of 62 run, 2
  failed, wall 223.2 s, the two being unit_cxx_passthrough and
  unit_cxx_partial on cxx with KeyError 'cxx.shlib' (their modules were
  interpreted after the change of the runtime); with --prepare the two
  pass (prepare 28 s and 35 s, 2 of 2 run, 0 failed); func_eqconstr_a0b0c0_1
  (150 on each backend) and func_setfunctions (39 on each): 4 of 4 run, 0
  failed.  make -C docs html: build succeeded (the two docutils errors on
  "interpreter.import" are older).  Not run: the whole suite on both
  backends (the suite stage).  After the review (the dispatch stack, the
  'x' case, the four constructed-state tests, eqShapeG18): make stage and
  make overlay status 0; unit_py_checker.py 50 ok on py;
  unit_constraints.py 14 on each backend, ok with one expected failure;
  ./run_tests -j 2 --mem 4 --backend both unit_constraints.py
  unit_cxx_scheduler.py unit_py_checker.py unit_cxx_checker.py
  unit_setfunctions_semantics.py unit_setfunctions_bugs.py
  unit_setfunctions_bindings.py func_setfunctions.py: 16 of 16 run, 0
  failed, wall 61.1 s.
- Left to the owner.  (1) The merge of two classes bound to different
  terms fails the step; Remark 2.70 of the note leaves the merge open, and
  two distinct unevaluated terms (or a pair beyond the budget of 1024
  cells) count as different here, so a program that binds two variables
  to equal terms that are not yet evaluated and then unites them loses
  the value (eqShapeG18, an expected failure); the alternatives are a test of the terms by an equality
  (b1 =:= b2 as a guard of the root, as apply_binding guards) or the
  binding through the class of before.  (2) RuntimeState::update_binding
  keeps its void declaration in state/rts.hpp (the header is in the
  include closure of cyrt.hpp, so a Boolean signature moves the ABI
  stamp); nothing calls it now, and it goes with the next change of the
  stamp.  (3) The marker FREE_IN_CONFIGURATION is the character 'v' in
  constr.cpp and in rts_freevars.cpp; a shared declaration needs a header
  outside the include closure.  (4) The rule "free in the configuration"
  covers the equality steps alone; ($!), ensureNotFree and the strict
  primitives narrow a variable with a generator from another
  configuration as before (strictApp), a reading of Def 2.44 to 2.46 that
  the audit left out.  (5) tests/manifest.json has no entry for
  unit_constraints.py, and the counts of unit_cxx_scheduler.py (10, from
  9) and unit_setfunctions_bugs.py (28, from 29) moved; a calibration run
  updates them.  (6) The other findings of the audit stay open: the N.1
  look-ahead (issue 02 of the audit), the deferred form of the failure
  rule of the set functions (06, 08), and the decisions of its table.

2026-10-12: the occurs check at the union of two classes of free variables (the owner's decision of 2026-10-12; worktree wt/occurs)
- The source.  The Lean mechanization of the theory found that without
  an occurs check the Fair Scheme yields a value no instance of the goal
  produces: goal = let e = x =:= y in e ? k x y e, with k (S x') y e =
  (x' =:= y) &> (e &> False) and data N = Z | S N (Cycle.curry of the
  audit).  The right alternative narrows x to S x', unites x' with y, and
  then meets the shared cell x =:= y: the class of x would hold x', a
  variable inside the instance of x, which no finite term satisfies.
  KiCS2 3.5.0 gives True and False (observed through the harness).  The
  owner states that PAKCS 3.4.1 gives no value for the right alternative,
  so True alone; the harness could not run PAKCS here (it writes under
  /tmp, read-only in the sandbox), so that value was not observed in this
  lane.  Sprite gave True and then dumped core past its stack guard
  on the C++ backend (issue #131), or raised a recursion error on the
  Python backend.  The owner approved on 2026-10-12 a rule of the scheme
  and of Sprite with it: when a constraint step unites the class of x
  with the class of y, the merged class is tested for a cycle, a member
  whose generator instance (the choice tree with the inner free
  variables at the constructor arguments, as created so far) contains
  another member of the class; if there is one, the step fails as an
  inconsistent constraint does, and the configuration is dropped.  The
  approved values: goal True alone; goalR (the right alternative alone)
  no value; Fsb.g10 of the audit, (x =:= unJ y) &> (y =:= unJ x) &>
  True, no value; every other behaviour as before.
- The rule in the runtime (both backends, the same test).  occurs in
  state/rts_constraints.cpp and in rts_constraints.py, called after the
  union and the merge of the bindings and before the pairing
  (_constrain_equal_rec), whose recursion on a cyclic class never ends.
  The test is local to the union.  It walks the generator instance of
  each side (the generator of the variable of the constraint, or of the
  root of its class before the union; a side without one, undecided or
  fresh, has no instance): the choice nodes along the decided path
  (read_fp), the constructor cells, through forward cells, down to the
  inner free variables.  An inner variable of the merged class is the
  cycle.  An inner variable of another decided class is bound through
  that class (Def 2.74), so the walk continues into the generator of
  that class (its own, or that of its root; E5), once per class.  A
  choice the configuration has not decided ends the walk: the variable
  is not narrowed there, and its generator is no instance of it (a walk
  of both sides would report a cycle through an alternative the
  configuration may never take).  Nothing is scanned outside the union:
  no table, no scan of the ids united.  Two unions skip the test without
  loss.  A union of two variables already in one class (the fork's
  constrain_equal of a variable with its root; the pairing of two paired
  generators) identifies nothing new.  A union of a leaf with a fresh
  leaf of a clone made in the same pairing (the flag fresh of
  pair_generators and of _constrain_equal): the fresh leaf has no
  instance and lies in no instance outside the class the enclosing union
  tested, so that test covers it; without the skip the pairing of an
  instance of n cells walked the instance below every leaf, O(n^2) for a
  linear pairing (deepShared 20000 of the probe ran into the timeout of
  60 s; with the skip 0.25 s).  The signature of
  RuntimeState::constrain_equal is in state/rts.hpp (the include closure,
  the ABI stamp), so the flag is a parameter of the file-static
  constrain_equal_impl, which the method calls with false.
- The finding: the check at the union does not reach goalR and g10.  In
  Sprite the equality step expands a decided variable to its instance
  before the pair reaches the constraint store (replace_freevar with the
  marker FREE_IN_CONFIGURATION, issue #129: a variable whose group is
  decided is narrowed by its generator, or by a clone through its
  class).  In goalR, after x' =:= y, the step x =:= y becomes S x' =:=
  y, which narrows y by a fork; S x' =:= S y' becomes x' =:= y', which
  expands x' through its decided class to S x'2; and so on: every union
  the store sees pairs two fresh leaves (a trace of the Python backend
  with x = 0, y = 1, x' = 2: the unions 2 ~ 1, then 3 ~ 4, 5 ~ 6, 7 ~ 8,
  ...), and the cycle appears as an endless narrowing, never as a union
  of two classes.  g10 takes the same route.  In goal, the left
  alternative makes the constraint cell x =:= y while both are free, and
  the right alternative meets that cell at its root: the constraint step
  of D unites the class of x with the class {x', y}, and the check fails
  the step.  So goal gives True alone on both backends; goalR and g10
  run on without end as before (the timeout of 60 s on both backends).
  The approved values of goalR and g10 need a decision on the equality
  step, which the lane of 2026-10-11 left to the owner (item (4) of its
  entry): (A) the strict equality of a decided variable and a variable
  that is free in the configuration makes the constraint cell instead of
  expanding the decided side, so the union and its check happen (the
  free side is then bound through the class, without a fork; the values
  stay, the forks are fewer); or (B) an occurs check at the narrowing of
  a free variable against a constructor term in a strict equality (the
  term, as created so far, holds a variable of the class of the
  variable), whose walk of the term costs O(size) at every narrowing and
  makes xs =:= l, with l an evaluated list of n cells, O(n^2).  (A) keeps
  one rule at one site and is recommended; it changes the equality step
  of both backends (constr.cpp, constr.py), the Def 2.62 check of the
  checkers, and the behaviour the lane of 2026-10-11 set under #129.
  Until then TestNoUnion of unit_occurs.py pins the approved values
  under expectedFailure (a child of 30 s).
- The checker (both backends).  D15 (occurs check), named after the
  decision item the audit filed the infinite class under: no class
  holds a variable that lies inside the generator instance of another
  member, verified at every fork (each clone), constraint step and
  yield beside E5 and E11 (check_occurs in checker.cpp, _check_occurs
  in checker.py).  The classes checked are those of the ids united (C++)
  or of the parent table (Python), the decided ones; the instance of a
  class is the generator at its root, read as the walk of the runtime
  reads it.  A violation is reported, never a changed value.  The check
  was seen to fire on a constructed state alone (test_occurs_check of
  unit_py_checker.py and of unit_cxx_checker.py: x with the generator
  Nothing ?_x Just y, decided RIGHT, y united with x); the goals of
  unit_occurs.py run under the checker on both backends without a
  report.  The module docstring of
  checker.py and the checker section of DeveloperNotes.rst name the
  check.
- #131 (the core dump).  The pairing of the constraint store
  (constrain_equal, _constrain_equal_rec, pair_generators) recurses once
  per level of the instances, and the stack guard polls stack_exhausted
  at the T_FUNC cases of hnf and procN alone, so a recursion of the
  store ran past the guard into the C stack: goal before the check (the
  cyclic pairing), and after it deepPair 100000 of StackGuard.curry (let
  e = xs =:= ys in e ? (cells n xs &> e &> True), with cells a tail call
  that narrows xs to n cells; the pairing descends n levels): the probe
  gave True and dumped core, exit 139, in 0.9 s.  The small local fix:
  _constrain_equal_rec polls stack_exhausted and throws the
  EvaluationError of the guard ("stack limit of N bytes exceeded (flag
  stack_limit)"), which the bindings hand to Python as
  curry.EvaluationError.  deepPair 100000 gives True and the error now
  (0.4 s); test_deep_pairing_raises of unit_cxx_stack.py pins it at the
  limit of the test file.  The deferred form of the guard (drop the
  alternative, report when the queue is empty) needs the pairing to
  unwind and resume, and an iterative pairing (a worklist) changes the
  order of the ids of the clones; both stay open under #131.  A change
  in printed values: a pairing deeper than the stack limit ends with the
  error where it ran on in the C stack before (deepPair 20000: two
  values before, True and the error now, as a nested length of that
  depth gives); a pairing of 2000 levels keeps both values.
- Tests.  data/curry/Occurs.curry and unit_occurs.py (both backends; one
  child per class under prlimit and timeout, and one under the checker):
  TestSharedCell (goal; goalJ, the shape with data R = N | J R: True
  alone); TestControls (ctrlShared; ctrlChain, an instance that reaches
  another decided class; ctrlLeaf; ctrlPair, two narrowed variables
  paired leaf by leaf; fiveCells, the g16 of Constraints.curry: the
  values of before); TestNoUnion (goalR, g10: the expected failure
  above); TestChecker.  test_occurs_check of unit_py_checker.py (the
  Python backend) and of unit_cxx_checker.py (the C++ backend, on a
  constructed state through the debug entries of the bindings: x with
  the generator Nothing ?_x Just y, y with Nothing ?_y Just x, both
  decided RIGHT, no report apart, D15 at the yield once united);
  test_deep_pairing_raises of unit_cxx_stack.py with deepPair and cells
  of StackGuard.curry (the C++ backend).
- Verification (the clean environment, CCACHE_DIR set, prlimit, at most
  two test processes at a time).  Before the change, under sprite-exec
  with steps:65536, prlimit 2 GB and timeout 60: Cycle goal True then a
  core dump on cxx (exit 139), True then a recursion error on py (exit
  1); goalR the timeout on both; g10 and g10v the timeout on both;
  KiCS2 through the harness: goal True and False, goalR False, g10 True;
  PAKCS through the harness: exit 4, read-only /tmp.  After the change:
  goal True alone, exit 0, on both backends, and under checker:true on
  cxx; goalJ True; ctrlShared, ctrlChain, ctrlLeaf True twice; ctrlPair
  (True, True), (False, False); fiveCells the two values of g16; goalR
  and g10 the timeout on both.  make stage status 0 (four runs), make
  overlay status 0 (four runs).  Single files: unit_py_checker.py 51 ok on py (50
  before); unit_occurs.py 9 ok with 1 expected failure on each backend;
  unit_cxx_stack.py 17 ok on cxx (16 before); unit_cxx_checker.py 58 ok
  on cxx (57 before), 58 ran on py with 54 skipped.  Through the runner
  (./run_tests -j 2 --mem 4): on py, unit_constraints,
  unit_py_constraints, unit_free_markers, unit_funpat_free_argument,
  unit_funcpat_narrowed, unit_fingerprint, unit_equality, unit_goals,
  unit_setfunctions_bindings, func_funpat and the six func_eqconstr
  files: 16 of 16 run, 0 failed, wall 41.0 s; on cxx the same files with
  unit_occurs, unit_cxx_freevars and unit_cxx_checker: 18 of 18 run, 0
  failed, wall 84.6 s; unit_py_checker and unit_cxx_stack on py: 2 of 2,
  0 failed; on both backends, unit_setfunctions_semantics,
  unit_setfunctions_bugs, unit_cxx_scheduler, unit_py_residuation,
  unit_cxx_runtime, unit_py_evaluation, func_setfunctions,
  unit_cxx_interp, unit_api, unit_examples and unit_prelude: 22 of 22
  run, 0 failed, wall 136.6 s.  Not run: the whole suite on both
  backends (the suite stage).
- Left to the owner.  (1) The equality step for goalR and g10, (A) or
  (B) above.  (2) The deferred form of the guard in the constraint
  store, under #131, and the acceptance of the changed output it brings
  today: a pairing deeper than stack_limit ends with the guard's error
  at once (an EvaluationError, not the dropped alternative of the
  deferred form), where it ran on in the C stack before; deepPair 20000
  gave two values and gives True and the error now.  (3) tests/manifest.json has no entry for
  unit_occurs.py; a calibration run adds it.  (4) On the Python backend
  a pairing deeper than its recursion limit ends with RecursionError,
  not curry.EvaluationError.  (5) The quadratic walk remains for the
  union of two decided classes with deep instances (both sides with
  generators before the union): the test at each leaf pair walks the
  instance below it, which the example of a cycle that only an inner
  pair shows (a1 = S c with c ~ b1, united through A = S a1 and B = S
  b1) needs.

2026-10-12: the pairing of a decided variable with a free one in a strict equality (decision D25 of 2026-10-12; worktree wt/pairing)
- The source.  The lane of the occurs check (above) found that the check
  at the union does not reach goalR and g10 of Occurs.curry: the equality
  step expanded a decided variable to its instance before the pair
  reached the constraint store, and the cycle appeared as an endless
  narrowing of fresh leaves.  The owner decided on 2026-10-12 (D25, the
  reading (A) of that entry, the revision 3 of the theory): when a step
  of =:= meets, on one side, a variable y that is free in the
  configuration (no decision of the fingerprint for its group, no
  binding) and, on the other side, a variable x the configuration decides
  (its generator was written by the narrowing of an earlier step, and the
  fingerprint decides its top choice, so that x reads as the constructor
  term of the decided branch, S x'), the step records the pair {x, y},
  the constraint cell it makes for two free variables, instead of
  narrowing y against the instance.  The constraint step of D then unites
  the two classes, with the occurs check, and y reads the instance of the
  decided class through the class (the machinery of #127).  A free
  variable against a constructor term that is not the instance of a
  decided variable narrows as before.  The two readings give the same
  values; the pairing forks less; and a cyclic pair fails the step where
  the narrowing ran without end: goalR and g10 give no value and end, as
  the owner states PAKCS has them (KiCS2 3.5.0 gives goalR False and g10
  True, observed through the harness).
- The rule in the runtime (both backends, the same rule).  The strict
  equality reads both sides before it expands a decided one.  C++
  (currylib/prelude/constr.cpp): constrEq_step asks hnf_or_free with a
  second marker, FREE_OR_DECIDED (a value set of kind 'd' beside
  FREE_IN_CONFIGURATION, kind 'v'), which replace_freevar
  (state/rts_freevars.cpp) reads: a variable the fingerprint decides is
  reported as a free one is, and a binding alone is copied in.  The step then
  classifies each variable side with decided (no binding of the group,
  is_narrowed) and acts: a free variable against a constructor or a
  value narrows as before (make_guides); a decided variable against a
  constructor or a value is expanded (expand: hnf_or_free with
  FREE_IN_CONFIGURATION writes its generator into the slot, rule S.x,
  and the pull-tab follows); two decided variables expand the left one
  first, and the step runs again with its instance (as before); two free
  variables, or a free one and a decided one, make the pair.  Python
  (currylib/prelude/constr.py): the same cases over hnf_or_free(free=True,
  decided=True); hnf and hnf_or_free of eval/fairscheme.py gain the
  keyword decided, which turns the slot write of a decided variable off
  under free.  The non-strict equality =:<= is not in the decision and
  keeps FREE_IN_CONFIGURATION: a decided left side is expanded before the
  right side is read, as before; the shared code path forced no change
  there.  No change to the object format, the ABI stamp (rts.hpp is
  untouched), a default, a public API or a flag; the design of the
  runtime, the scheduler and the collector stays.  An order of steps
  changed without a change of values: the expansion of a decided side
  waits until the other side is read, so when the other side forks, the
  decided side is expanded in each clone (the expansion of a decided
  choice is a fork() with one surviving clone, counted in forks_total).
  After a case on x the body of the rule reads x as the constructor cell
  of the decided branch (the pull-tab copies the leaf of the generator
  into the slot), which the step takes for a constructor term: y is
  narrowed against it by a fork, and the equality below it, x' =:= y',
  meets the two variables and pairs them, so a cycle through such a cell
  fails one level down (cycleLeaf of Pairing.curry), where the pure
  paired form (cycleVar, goalR, g10) fails at once.  The implemented
  reading of "the instance of a decided variable" is therefore the free
  variable node whose group the fingerprint decides; a matched argument
  cell is a constructor term to the step.  The review of the lane
  confirmed the gap and asked for no code change.
- The checker (both backends).  The check Def 2.62 (equality step), at
  an instantiation and at a slot write inside a step of constrEq or
  nonstrictEq (instantiate_begin of checker.cpp,
  _check_equality_narrowing of checker.py), reads the new rule: a step
  of constrEq that narrows a variable that is free in the configuration
  against a free variable is reported, and the report names the other
  variable when the configuration decides it ("the step pairs them
  (decision D25)"); a slot write that replaces a decided variable by its
  generator against a variable that is free in the configuration is
  reported too, the clause that fires on the old step (the expansion of x
  against the free y of goalR); a narrowing against a constructor term
  passes.  D15, E5 and E11 are unchanged.  The module docstring of
  checker.py and the checker section of DeveloperNotes.rst name the new
  clause.
- Tests.  data/curry/Pairing.curry and unit_pairing.py (both backends;
  one child per class under prlimit and timeout, and one under the
  checker): leftDecided and rightDecided (S _a, one value), bothBranches
  (Z and S _a, a value per decided branch), readThrough ((S _a, True),
  the free variable read through the merged class), deeper ((S Z, S Z),
  the inner variable narrowed after the union), bothDecided ((S Z, S
  Z)), againstTerm ((S Z, Z)), decidedAgainstTerm (S Z), cycleVar and
  cycleLeaf (no value); the values and their counts are those of the
  narrowing reading.  unit_occurs.py: TestNoUnion is TestPairedCycle,
  goalR and g10 give no value, plain and under the checker, without the
  expectedFailure.  The cycles of unit_pairing.py run in their own
  class, TestPairedCycle, in a child with a 30 s timeout, as in
  unit_occurs.py: a regression to the endless narrowing is killed there
  and the values of the finite goals stay visible.  The guard of _expand
  in constr.py is a raise, not an assert, so that it survives python
  -O.  test_equality_step_pairs of unit_py_checker.py (the
  Python backend) and of unit_cxx_checker.py (the C++ backend), on a
  constructed state: x decided LEFT with a generator, y free, the redex
  constrEq x y; the slot write of x against y and the narrowing of y
  against x are reported, the narrowing of y against True is not.  The
  Python test was run against the sources of 0a29d91a (the package put
  first on sys.path, the built bindings linked beside it) and failed
  there with "InvariantViolation not raised".
- Verification (the clean environment, CCACHE_DIR set, prlimit, at most
  two test processes at a time).  Before the change, under sprite-exec
  with steps:65536, prlimit 2 GB and timeout 60, on both backends: Occurs
  goal True, exit 0; goalR and g10 the timeout, exit 124.  KiCS2 through
  the harness: goalR False, g10 True; PAKCS through the harness: exit 4
  (it writes under /tmp, read-only in the sandbox), so its values were
  not observed in this lane.  make stage status 0, make overlay status
  0.  After the change, on both backends: goalR and g10 no value, exit 0;
  goal True; the same under checker:true on cxx.  Single files, each ok:
  unit_pairing.py 11 on each backend, before and after the review (the
  cycles moved to their own class); unit_occurs.py 10 on each backend
  (9 ok and 1 expected failure before).  Through the runner (./run_tests
  -j 2 --mem 4 --backend both): unit_constraints (14 on each backend),
  unit_py_checker (52 on py, 51 before), unit_cxx_checker (59 on cxx, 58
  before): 6 of 6 run, 0 failed, wall 51.0 s; unit_cxx_freevars,
  unit_free_markers, unit_funpat_free_argument, unit_funcpat_narrowed,
  unit_py_residuation, unit_setfunctions_bindings, unit_fingerprint,
  unit_equality, unit_goals, unit_py_constraints, func_funpat and the six
  func_eqconstr files: 34 of 34 run, 0 failed, wall 75.1 s;
  unit_cxx_scheduler, unit_setfunctions_semantics, unit_setfunctions_bugs,
  unit_cxx_runtime, unit_cxx_stack, unit_py_evaluation,
  func_setfunctions, unit_cxx_interp, unit_api, unit_examples,
  unit_prelude: 22 of 22 run, 0 failed, wall 129.1 s.  make -C docs
  html: status 0.  Not run: the whole suite on both backends (the suite
  stage).
- Changed printed values.  The goals that narrowed without end give no
  value and end: goalR and g10 of Occurs.curry, cycleVar and cycleLeaf of
  Pairing.curry.  No other value of the test corpus changed.  The forks:
  a strict equality of a decided variable and a free one makes no fork
  where it forked once per constructor of the type before (the clones of
  the other constructors failed); a decided side whose other side forks
  is expanded once per clone instead of once before the fork.  The counts
  were not measured.
- Left to the owner.  (1) tests/manifest.json has no entry for
  unit_pairing.py or unit_occurs.py; a calibration run adds them.  (2)
  The matched-argument form costs one fork per pattern level more than
  the paired form: the runtime has no map from a leaf of a generator to
  its variable, and a map needs state beside the object format; the
  values are the same.  (3) The non-strict equality keeps its order (a
  decided left side is expanded before the right side is read); a
  decision on =:<= would mirror the rule.  (4) The values of PAKCS for
  goalR and g10 were not observed here.  (5) The whole suite on both
  backends (the suite stage).

2026-10-12: the failure rule F2 of the set functions in its deferred form, and the flag setfunction_failures removed (the owner's decision of item 1 of the briefing of the audit; worktree wt/setfail)
- The decision.  The owner decided on 2026-10-12 that the failure rule F2
  of the set functions, in the deferred form of Definition 2.94 of the
  note on the Fair Scheme, is Sprite's one behaviour on both backends,
  and that the interpreter flag setfunction_failures with its two values
  escape and encapsulate goes.  The semantics was fixed at the outset
  (decision D1 of 2026-10-09: a set function whose computation fails
  because of an outside argument is itself undefined, the weakly
  encapsulated search of Christiansen, Hanus, Reck and Seidel, as KiCS2
  behaves); the flag was scaffolding, and both of its values were wrong:
  'escape' failed the capsule at the first boxed failure a step met, so
  the value depended on the order of the alternatives, and 'encapsulate'
  never failed a capsule.  Sprite implements the semantics with no flag
  and no name for it.
- The rule in the runtime (both backends, the same rule).  (d1) When a
  configuration of a capsule meets a failure, the configuration is
  dropped, as every failure drops a configuration, and the capsule
  records the failure on its queue (the queue object the SetEval node
  holds; one record per inner queue).  A failure whose path from the
  root of the configuration crosses a set guard, in the levels of the
  scan or in the guards of the inductive position (the test
  failure_escapes made before, unchanged), is an outside failure: the
  record names the sets of the enclosing guards it crossed, less the
  current set, with its level, the position in the chain of the
  capsules, innermost first, of the outermost capsule whose guard it
  crossed (zero when the failure crossed the guard of the capsule
  alone).  A failure that crossed no guard is an inside failure.  (d2)
  On the empty queue the capsule is a failure when it produced no value,
  recorded no inside failure and recorded some outside failure; its set
  stands otherwise, the empty set included.  (d3) The failure written
  carries the guards of the records at the least level, the failures
  that entered the chain innermost (deviation 147 of the README of the
  Lean development), together; a record of level zero makes a plain
  failure, which drops the alternative of the enclosing function.  C++:
  Queue gains produced, inside_failure and outside_failures
  (state/queue.hpp, with FailureRecord; Queue::split and Queue::clone
  copy the record to both queues, as Capsule.split of the mechanization
  keeps it); RuntimeState::record_failure (state/rts_setfunctions.cpp)
  replaces failure_escapes and fail_capsule and is called at the three
  sites that observe a failure, procD at the root (a failed alternative
  rewritten to Fail at the root passes neither procN nor hnf), procN at a
  level of the scan and hnf at the inductive position (fairscheme.cpp);
  the configuration then drops as before.  A failure is observed first
  with its whole path and again at each level above it on the way to the
  root (hnf forwards the redex to the failure and the caller meets it at
  the redex), so Configuration::failure_recorded makes the record once; a
  clone starts with its own flag.  A constraint that fails at the root of
  procD (the occurs check, or the union of two classes with different
  instances; constrain_equal returns false and the case falls through to
  T_FAIL) is a failure at the root like a failure cell there: its path
  crossed no guard, so the record is an inside failure (the review of
  2026-10-12 found the fall-through without a record; the first version
  of this lane called it deliberate).  D of the Python backend records
  the same way; there a configuration that stripped a guard of its set at
  the root before (escape_all) records an outside failure of level zero
  instead.  A constraint lifted from under a guard (lift_constraint)
  carries no guards: the lift copies the spine with its guards below the
  constraint, so a constraint of the argument that fails at the root is
  recorded as the function's own; see "Left to the owner".  The verdict
  is in allValues_step
  (currylib/setfunctions.cpp) on the empty queue, the one place the end
  of a queue is observed: the values of a set are produced one at a time
  (a cons cell per value, nil at the end), so isEmpty returns at the
  first value without running the queue to its end, and a set that
  produced a value never fails.  The status E_SETFAIL, the pending-control
  path of procD and procN, and capsule_failure_guards are gone.  Python:
  Queue of eval/queue.py has the same fields and copies them in __copy__
  (split_queue and clone_queue copy the queue); record_failure of
  eval/rts_setfunctions.py replaces boxed_failure, called by D, N and
  hnf of eval/fairscheme.py; allValues of currylib/setfunctions.py gives
  the verdict and counts the values; E_SETFAIL of generic/eval/control.py
  is gone; Configuration.failure_recorded as in C++.
- The level reading, in two functions per backend.  The choice of the
  records is the free function cyrt::failure_guards (state/queue.cpp,
  declared in queue.hpp; Queue::failure_guards calls it) and the module
  function failure_guards of eval/queue.py (Queue.failure_guards calls
  it): the least level, and the union of their guards.  The level of a
  record is RuntimeState::failure_level (state/rts_setfunctions.cpp,
  declared in rts.hpp) and failure_level of eval/rts_setfunctions.py
  (bound on the state in rts.py): the position, innermost first, of the
  outermost capsule of the chain among the sets crossed.  record_failure
  calls failure_level, and the checker calls both (check_failure_record
  the level, check_capsule_end the choice), so the checker never holds a
  copy of the reading (the review of 2026-10-12 found it computed in six
  functions).  The comments name the choice and the deviation.  The
  owner's item 2(a) may confirm or change the reading: a change of the
  choice of the records touches the two failure_guards, and a change of
  the level touches the two failure_level, and nothing else.
- The removal, every file.  src/python/interpreter/flags.py: the entry of
  the docstring and the row of FLAG_INFO.  src/cyrt/fwd.hpp: the enum
  SetFFailures and the status E_SETFAIL.  src/cyrt/state/rts.hpp and
  rts.cpp: the constructor parameter, the member setfunction_failures and
  capsule_failure_guards; the declarations of failure_escapes and
  fail_capsule became record_failure.  src/cyrt/state/rts_setfunctions.cpp,
  fairscheme.cpp, currylib/setfunctions.cpp: the two code paths and the
  comments.  src/python/backends/cxx/cyrtbindings/constants.cpp: the
  Python enum; graph.cpp: the argument of both constructors of
  RuntimeStateBase (the comment on the positions of the rotation cadence
  moved by one); __init__.py: _SETF_FAILURES and the attribute.
  src/python/backends/py/eval/rts.py: the attribute, and the method table
  (record_failure, failure_level for boxed_failure).  control.py,
  fairscheme.py, rts_setfunctions.py, currylib/setfunctions.py,
  callstack.py, walkexpr.py: the code and the comments that named the
  flag or boxed_failure.  docs/source/PythonAPI/CurryPackage.rst: the
  row of the table of the flags.  docs/source/CurryPrimer/Semantics.rst:
  the paragraph on a failure that comes from an argument states the one
  rule in three sentences and names no removed value.
  docs/source/DeveloperNotes.rst: the list of the hooks of the checker.
  tests/data/curry/benchmarks/results/README.md: the record
  phase3-2026-10-10 says what the measured runs used, in the past tense,
  without a flag name.  curry/lib/Control/SetFunctions.curry: the comment
  of applyS (a comment alone: the .icy made from the edited source with
  sprite-make --icy in a scratch tree under SPRITE_DISABLE_SYSLIB_CHECKS=1
  is byte-identical to the committed one, cmp; the digest of the source
  in tests/unit_currylib.py is updated).  tests/data/curry/
  SetFunctionsSemantics.curry: the comments of the header and of section
  3.  A flags string that names setfunction_failures is rejected as every
  unknown flag is: Interpreter(flags={'setfunction_failures': ...})
  raises ValueError "unknown flag: 'setfunction_failures'"
  (test_setfunction_failures_is_unknown of unit_api.py), and
  SPRITE_INTERPRETER_FLAGS drops it with the warning of an unknown flag.
  The harness of the Fair Scheme proofs (outside this repository) names
  the systems cxx-escape and cxx-encapsulate, which set the flag; they
  need the owner's edit.
- The ABI stamp.  fwd.hpp, rts.hpp, queue.hpp and configuration.hpp are in
  the include closure of cyrt/cyrt.hpp, so the stamp of the compiled
  Curry objects moves, as the decision accepts: every compiled object is
  stale, make stage compiled the library again, the product cache misses,
  and the suites need ./run_tests --prepare-only first.  No change to the
  object format itself, no new flag, no default, no public API beyond the
  removal.
- The checker (both backends).  Two hooks: failure_recorded, after
  record_failure made its record (the hook reads the guards the path
  crossed from the scan and the inductive position, or from the call
  stack, as the runtime does, and check_failure_record compares: an
  inside failure crossed no guard; an outside failure names the crossed
  sets less the set of the capsule, with the level of the outermost
  capsule of the chain among them), and capsule_end, after the verdict of
  allValues (check_capsule_end: the failure is written iff the capsule
  produced no value, recorded no inside failure and recorded some
  outside failure, (d2); its guards are the union of the records at the
  least level, (d3)).  The reports name F2 (d1), F2 (d2) and F2 (d3).
  S0, S-split and capsule_entry are unchanged.  The debug entries
  checker_debug_check_failure_record and checker_debug_check_capsule_end
  of the bindings serve the constructed-state tests test_failure_record
  and test_capsule_end of TestSetFunctions in unit_cxx_checker.py and
  unit_py_checker.py (two capsules on the queue stack; each report
  provoked once, and the passing records).  The module docstring of
  checker.py and the developer notes name the hooks.
- Tests.  New: tests/data/curry/SetFunctionsFailures.curry and
  tests/unit_setfunctions_failures.py (both backends; one child per class
  under prlimit and timeout, and one under the checker, as
  unit_pairing.py does): t1, t2 [True]; t3, t4 False; d2a, d2b True;
  lazyT [True]; valThenFail [1]; mixed [2] (an inside failure, an outside
  failure and a value in one capsule); mixedE True; t1c [True]; argFail
  [0]; level, levelB []; levelC no value; level2 [[]]; ownflag [0];
  set0Inner no value; set0InnerV [[7]]; the pairs t1/t2, t3/t4, d2a/d2b
  and level/levelB asserted equal.  Every value is the value of KiCS2
  3.5.0, observed through the harness, except cycleInside True and
  cycleInsideS [] (test_cycle_inside; the goals are in FAILURES_GOALS of
  the checker tests too): an alternative that dies through the failed
  constraint step at the root alone (the cycle of x, decided outside the
  capsule by isT, with a free y through the union of the classes; no
  narrowing with a failing sibling inside), beside the failure of the
  argument.  KiCS2 has no occurs check, so it is no reference there; the
  value is that of rule (d1).  unit_setfunctions_semantics.py:
  one child per class and one column of values; TestFailureRule keeps
  its goals with the values of the semantics (s3, s3e, s4, s4h, s4a,
  s4b, s4c and nestFail no value, nestFailInner True, lazyFirst True; the
  'encapsulate' column, [] or True, went with the flag); TestDefault,
  which pinned the flag table and the default, went (it tested the flag
  itself).  unit_setfunctions_bindings.py: check_settings is
  check_values, one child per shape.  unit_cxx_rotation.py:
  test_counters_of_a_search pins countQueens 8 at (2112920, 511384)
  steps and forks under the deferred form (the immediate trigger took
  (2020742, 461457), the rule of the dissertation (2112920, 511484); the
  value is 92 under every rule); test_counters_of_a_search_under_encapsulate
  went (it tested the flag); the two direct constructions of
  RuntimeStateBase lost the argument.  unit_cxx_checker.py and
  unit_py_checker.py: test_setfunction_semantics runs the module once;
  test_setfunction_failures (the goals named in FAILURES_GOALS: the
  module lifts a local declaration of t1c into a private nullary
  function) runs the new module with and without the checker, in the
  compiled and the interpreted children.  unit_api.py:
  test_setfunction_failures_is_unknown.  unit_currylib.py: the digest.
- Verification (the clean environment, CCACHE_DIR set, prlimit, at most
  two test processes at a time).  Before the change, under sprite-exec
  with steps:65536, prlimit 2 GB and timeout 60, on both backends: t1,
  t2, t3, d2a, lazyT, valThenFail, level and set0Inner no value, t4
  False, levelB [].  KiCS2 through the harness: t1, t2 [True]; t3, t4
  False; d2a, d2b True; lazyT [True]; valThenFail [1]; level, levelB,
  level2 []; levelC, set0Inner, ordA no value; set0InnerV [[7]]; t1c
  [True]; ownflag [0]; mixed [2]; mixedE True; argFail [0].  make stage
  status 0 (53 s; the stamp moved), make overlay status 0.  After the
  change, on both backends, plain and under checker:true: every goal of
  SetFunctionsFailures.curry prints the value of KiCS2 (the C++ backend
  printed no value on d2a and d2b until the record at the root of procD
  was added).  Single files: unit_setfunctions_failures.py 23 tests ok
  on cxx and on py; unit_setfunctions_semantics.py 41 ok on each;
  unit_py_checker.py 55 ok on py (52 before); unit_cxx_checker.py 63 ok
  on cxx (59 before); unit_currylib.py 10 ok on each after the stage of
  the edited source.  ./run_tests --prepare-only for the selection, 4 of
  4 run, wall 60 s.  Through the runner (./run_tests -j 2 --mem 4
  --backend both): unit_setfunctions_failures, unit_setfunctions_semantics,
  unit_setfunctions_bindings, unit_setfunctions_bugs, unit_cxx_rotation,
  unit_cxx_scheduler, unit_cxx_checker, unit_py_checker, unit_api,
  func_setfunctions, unit_pairing: 22 of 22 run, 0 failed (unit_currylib
  failed twice in that batch, before the stage of the edited library
  source: the digest, and make install of the test regenerating a
  .json.z older than the source; both pass after make stage).  make -C
  docs html: build succeeded, 2 warnings (the two docutils errors on the
  target "interpreter.import" are older), and no page of the output names
  the flag.  Not run: the whole suite on both backends (the suite stage).
  The review fixes of 2026-10-12 (the record of a failed constraint, the
  shared failure_level and failure_guards): probes on the Python backend
  with a temporary trace of D and of record_failure (removed after)
  showed every cycle made by a narrowing inside the capsule beside a
  failure cell of a sibling alternative, and cycleInside with the
  fall-through alone: no value on the Python backend, True on the C++
  backend, which met a failure cell in a sibling there.  After the fix
  both backends print True and [] on cycleInside and cycleInsideS, plain
  and under checker:true (sprite-exec).  unit_setfunctions_failures.py:
  24 ok on cxx and on py; with the record of D of the Python backend
  taken out again, 2 failures on py (test_cycle_inside and
  TestChecker.test_one_capsule), then 24 ok with it back.
  unit_cxx_checker.py 63 ok on cxx; unit_py_checker.py 55 ok on py.
  make stage status 0 (rts.hpp and queue.hpp changed: the stamp moved
  again), make overlay status 0.  ./run_tests
  --prepare-only --backend both for the selection: 4 of 4 run, 0 failed,
  wall 57 s.  Through the runner (./run_tests -j 2 --mem 4 --backend
  both): unit_setfunctions_failures, unit_setfunctions_semantics,
  unit_setfunctions_bindings, unit_setfunctions_bugs, unit_cxx_rotation,
  unit_cxx_scheduler, unit_cxx_checker, unit_py_checker, unit_api,
  unit_currylib, func_setfunctions, unit_pairing: 24 of 24 run, 0 failed,
  wall 107 s.
- Changed printed values.  t1, t2 (sortValues (set1 (\a -> a ? True)
  failed) and the swap): no value before, [True] after.  t3 (isEmpty of
  t1): no value before, False after; t4 False before and after.  d2a,
  d2b: no value before, True after.  lazyT: no value before, [True]
  after.  valThenFail: no value before, [1] after.  level: no value
  before, [] after; levelB [] before and after.  levelC and set0Inner: no
  value before and after.  level2: [] before and after.  ownflag: [0]
  before and after.  mixed: no value before, [2] after; mixedE: no value
  before, True after.  argFail: [0] and no value before and after.
  cycleInside: no value on the Python backend and True on the C++ backend
  before the record of a failed constraint, True on both after;
  cycleInsideS [] on both after.  The
  goals of the set-function test modules that must have no value (s3,
  s3e, s4, s4h, s4a, s4b, s4c and nestFail of SetFunctionsSemantics.curry;
  TestFailureRule of unit_setfunctions_semantics.py pins them on both
  backends) have none.  The
  counters of countQueens 8 moved from (2020742, 461457) to (2112920,
  511384), the value 92 unchanged; QueensSet and the other benchmark
  programs with set functions were not measured here.
- Left to the owner.  (1) Item 2(a): the level reading; the choice of the
  records in cyrt::failure_guards (state/queue.cpp) and failure_guards of
  eval/queue.py, the level in RuntimeState::failure_level and
  failure_level of eval/rts_setfunctions.py; the checker calls them.
  (1b) A constraint lifted from under a guard loses the guards it
  crossed: sortValues (set1 id cyc) with cyc = (x =:= T x) &> True where
  x free, data T = T T, prints [] on both backends (the occurs check of
  the argument fails at the root with no guard on the path) where the
  failure of the argument means no value, as set1 id failed has none; a
  record of the guards at the lift would settle it, outside this lane.
  (1c) The generator of a free variable escapes as a choice with a
  failing side: sortValues (set1 (\a -> a ? True) cyc) prints [True]
  twice, one value per side of the escaped generator of x.  (2) The records of a queue go to both queues of
  a split and to a clone, as the mechanization keeps them; a record made
  by a configuration that had decided the escaped choice before the
  split is kept by the queue of the other side as well.  (3) The harness
  systems cxx-escape and cxx-encapsulate name the removed flag.  (4)
  tests/manifest.json has no entry for unit_setfunctions_failures.py,
  and the counts of unit_setfunctions_semantics.py (41),
  unit_cxx_rotation.py (20), unit_cxx_checker.py (63), unit_py_checker.py
  (55) and unit_api.py (14) moved; a calibration run updates them.  (5)
  The whole suite on both backends (the suite stage).  (6) The next run
  of run_benchmarks against the records moves the counters of the
  programs with a failing boxed alternative.

2026-10-12: the equality guard at the merge of two bound classes (decision D26 of 2026-10-12; worktree wt/boundmerge)
- The decision.  The constraint store keeps, per configuration, the
  classes of free variables that strict equalities united and the
  bindings that non-strict equalities (=:<=) gave them, one binding per
  class under its root (issue #128).  The rule of #128 failed the step
  when a union joined two classes bound to distinct cells, and that lost
  a value when the two cells were equal terms not yet evaluated:
  eqShapeG18 of Constraints.curry gave (0, 0) alone where the procedure
  of Antoy and Hanus, and KiCS2, give (0, 0) and (2, 2).  The owner
  decided on 2026-10-12 (D26, "run it as you say"): when a union joins
  two classes that both carry a binding and the two binding cells are
  not one cell, the union goes ahead, the merged class keeps the binding
  of the root the union keeps, and the configuration continues with the
  strict equality of the two bindings, b1 =:= b2, as a guard above the
  result of the constraint cell: the result becomes (b1 =:= b2) &> result
  in effect.  The guard is an ordinary strict equality of the program:
  its failure drops the configuration (g18 and g18t keep (A, A) and
  False alone, g5 keeps no value), its success yields True in its place
  (eqShapeG18 gains (2, 2)), and its own pairings proceed as usual.  One
  cell under both roots is one binding, and a union with one bound class
  moves the binding: neither makes a guard.  The rule applies at every
  union of a constraint step, the recursive ones of the pairing of two
  generators included; three or more bindings at one step make one guard
  per extra binding, nested under &>.  The note (revision 3, def:b-Dbeta),
  the proofs and the Lean mechanization carry the same rule; this lane is
  the Sprite part.
- The rule in the runtime (both backends, the same rule).  C++:
  merge_binding (state/rts_constraints.cpp) moves the binding of the old
  root to the new root when the new root has none, erases the old key
  when the two roots hold one cell, and otherwise keeps the binding of
  the new root, erases the old one and puts the pair (dropped, kept) into
  the list of the step (MergeGuards), which constrain_equal_impl,
  pair_generators and _constrain_equal_rec thread through the pairing;
  the four-argument _constrain_equal_rec of get_generator and the
  four-argument constrain_equal of the fork pass no list, and a merge of
  two distinct cells there fails the step as before (no program is known
  to reach it: the two generators of one class are paired when they meet,
  so their leaves share their classes, and the leaves of a clone are
  fresh).  The review asked for the reach to be visible: the checker
  counts such a merge (unguarded_merges, Checker::unguarded_merge and
  checker.unguarded_merge, called from merge_binding and update_binding
  when the list is absent and the cells differ), TestGuardCounts of
  unit_boundmerge.py pins the count at zero for every goal of the module,
  and test_unguarded_merge_counted of unit_cxx_checker.py (through the
  new entry checker_debug_update_binding, which runs the public
  RuntimeState::update_binding, the no-list form) and of
  unit_py_checker.py drives the count to one on a constructed state.
  get_generator says in a comment that the result of the pairing is
  dropped on purpose.
  The root form RuntimeState::constrain_equal(C, Cursor), the one procD
  calls, collects the pairs and, when there are some, writes into the
  slot a copy of the constraint cell whose result is (b1 =:= b2) &> ...
  &> result, with the tables constrEq_Info and CyI7Prelude12BindingGuard
  of the runtime, the first union outermost; procD then writes that
  result in place of the cell as it does without a guard, so procD is
  unchanged.  The private copy of the spine: the slot is the root of the
  configuration, private to it, because lift_constraint copied the spine
  of the configuration to bring the constraint cell to the root, and the
  copy of the cell is the one lift_constraint makes
  (Node::create(constr->info, value, constr->pair)); the shared cell,
  which other configurations with other stores hold, is not written.
  No header of the include closure of cyrt/cyrt.hpp changed (checker.hpp
  is outside it), so the ABI stamp stayed.  Python: update_binding
  (rts_bindings.py) takes the list, constrain_equal and
  _constrain_equal_rec (rts_constraints.py) thread it, and D of
  fairscheme.py wraps the value in Node(&>, Node(constrEq, b1, b2),
  value) per pair and sets rts.E, the private root of the configuration.
  same_binding (C++) and _same_binding (Python) serve add_binding alone
  now, the second non-strict binding of a bound class; the merge no
  longer compares the terms.
- The checker (both backends).  D26 (guard at the merge):
  constrain_begin, called by the root form of constrain_equal (C++) and
  by D before the call (Python), records the bindings of the
  configuration; merge_guard, called at each merge of two distinct
  cells, records the pair (and counts merge_guards); constrained runs
  check_merge_guards after E11, E5 and D15 (unchanged): every binding
  cell of the record the configuration no longer holds is an operand of
  a recorded pair, each recorded pair holds two distinct cells of the
  record, and the continuation of the configuration (the result of the
  constraint cell at the root in C++, where constrained runs before the
  write of procD; the root in Python, where it runs after the write)
  begins with one &> cell per pair, the first recorded outermost, whose
  condition is the constrEq cell over the pair.  The first version took
  the bindings at the dispatch, which is too early in C++: procD runs
  every step of a configuration under one dispatch (goto redoD), so the
  bindings made before the constraint cell reached the root were missing
  from the record, and eqShapeG18 was reported under checker:true.  New
  debug entries of cyrtbindings: checker_debug_unbind,
  checker_debug_constrain_begin, checker_debug_merge_guard,
  checker_debug_merge_guard_node, checker_debug_check_constrained.
- Tests.  tests/data/curry/BoundMerge.curry and tests/unit_boundmerge.py
  (12 tests on each backend): evalEq (two Just True cells, evaluated and
  equal), unevalEq (two sums 1 + 1, the shape of eqShapeG18), diffVal
  (1 + 1 against 1 + 2: no value from the alternative), sameCell (one
  let-bound sum under both classes: no guard), leafEq and leafDiff (the
  shape of g18 over data T = A | B Int, equal and different leaves),
  three and threeDiff (the leaf classes {x1}, {x2, y1} and {y2} of
  x = P x1 x2 and y = P y1 y2, each bound, united at one step: two
  guards), narrowIn (Just z =:= Just True narrows z) and narrowLeaf (B z
  =:= B (1 + 1)).  The expectations are the values of KiCS2, observed
  through the harness (run.py --system kics2) on 2026-10-12, with the
  value counts; TestChecker runs the goals under checker:true;
  TestGuardCounts evaluates each goal in process under the checker and
  pins the count merge_guards (1, 1, 1, 0, 1, 1, 2, 2, 1, 1).  Two
  findings while the module was written: the translation to ICurry
  folds not False, and a call ident True of ident b = b, to the
  constructor True, and a literal constructor is one static cell on the
  C++ backend and a fresh cell per occurrence on the Python backend, so
  a binding written as True or not False makes no guard on the C++
  backend; the unevaluated bindings of the module are sums, which the
  translation keeps.  unit_constraints.py:
  TestMergeOfEqualUnevaluatedTerms lost its expectedFailure and joined
  the checker child; the comments of Constraints.curry on g18 and
  eqShapeG18 follow the rule.  test_guard_at_the_merge of
  unit_cxx_checker.py (TestFreeVariables) and of unit_py_checker.py
  (TestAuditChecks), on a constructed state: a binding dropped without a
  report, a report without the guard in the continuation, and a report
  with a cell the configuration never held are each reported; the guard
  above the result is not.  Before the change, sprite-exec gave
  eqShapeG18 (0, 0) alone on both backends; after it (0, 0) and (2, 2),
  plain and under checker:true; g18 (A, A), g18t False, g5 no value
  before and after, and KiCS2 the same four results.  The ten goals of
  BoundMerge print the values of KiCS2 on both backends, plain and under
  checker:true (sprite-exec, rotation steps:65536).  One file at a time:
  unit_boundmerge.py 12 ok on cxx and on py; unit_constraints.py 14 ok on
  both; unit_cxx_checker.py 64 ok on cxx; unit_py_checker.py 56 ok on py.
  make stage status 0 (three times, after each C++ edit), make overlay
  status 0.  Through the runner (./run_tests -j 2 --mem 4 --backend
  both): unit_constraints, unit_boundmerge, unit_pairing, unit_occurs,
  unit_cxx_freevars, unit_free_markers, unit_funpat_free_argument,
  unit_funcpat_narrowed, unit_py_residuation, unit_setfunctions_bindings,
  unit_cxx_checker, unit_py_checker: 24 of 24 run, 0 failed, wall 69 s.
- Changed printed values.  eqShapeG18: (0, 0) before, (0, 0) and (2, 2)
  after, on both backends.  g18 (A, A), g18t False, g5 no value, eqJust
  (Just True, Just True), orphan (True, True), g14, g16, g16b, g20, g22,
  g15 and strictApp unchanged.  The direct second binding of a bound
  class, (x =:= y) &> (x =:<= (1 + 1)) &> (y =:<= (1 + 1)) &> (x, y),
  prints (2, 2) on both backends and in KiCS2 (the bound side reads its
  binding before the step): no merge is involved.  No benchmark was
  measured; the counters of the programs with a merge of two bound
  classes move by the steps of the guards.
- Left to the owner.  (1) tests/manifest.json has no entry for
  unit_boundmerge.py, and the counts of unit_cxx_checker.py (64),
  unit_py_checker.py (56) and unit_constraints.py (14, one expected
  failure fewer) moved; a calibration run updates them.  (2) The whole
  suite on both backends (the suite stage).  (3) The folding of ident
  True to True by the translation to ICurry (flat2icurry, the port of
  icurry 3.1.0): a note for the test authors, not a bug of this lane.
  (4) A merge of two distinct cells outside the constraint step at the
  root (the fork's constrain_equal of a variable with its root, the
  pairing of get_generator) keeps the failure of #128; no program is
  known to reach it (see above), the checker counts a reach
  (unguarded_merges) and the tests pin the count at zero; a guard there
  needs the root slot of the configuration, which both callers have,
  when one is found.

2026-10-12: the look-ahead of rule N.1 of the Fair Scheme (issue #132; worktree wt/lookahead)
- The source.  The audit of 2026-10-11 found that both backends drop the
  look-ahead of rule N.1: the rule reads every successor of the root of a
  constructor-rooted configuration before any step, and a failure among
  them, dereferenced, means that the configuration has no value, so rule
  D.5 drops it.  Both backends tested the failure at the cursor of the
  depth-first scan alone: the scan entered the first successor that is
  not a value, stepped it until the rotation interrupt, restarted from the
  root and entered the same successor again.  The reproducer, goal = let
  t = failed in (t ? loop, t) with loop = loop: rule D.2 forks (t, t) and
  (loop, t); the first child steps t to a failure and is dropped; the
  theory drops the second child at its dispatch, before any step at loop,
  and the run ends with no value.  Sprite did not end (exit 124 under
  timeout 60 on both backends), and KiCS2 does not end either.  The
  control goal0 = (t ? 1, t) ends with no value everywhere.  No value is
  lost; the departure is in termination.
- The rule in the runtime (both backends, the same rule).  Before the scan
  descends into a successor of a constructor cell, it reads the successors
  of the cell through forward cells; when one is a failure, the
  configuration takes the path a failure at the cursor takes.  C++
  (fairscheme.cpp, procN, the default case): failed_successor of the new
  header cyrt/fairscheme.hpp reads the pointer successors of the cell
  once, in order, without a write and without an allocation (a partial
  application is a value and is not read); a finding sets the tag to
  T_FAIL and goes to the case T_FAIL, so a boxed failure in a capsule
  fails the set function (failure_escapes, fail_capsule, as at the
  cursor), a failure under the root of the configuration returns T_FAIL
  to procD, which drops the configuration, and a failure under the redex
  of ($!!) makes the redex a failure as before.  The header is outside
  the include closure of cyrt/cyrt.hpp (the checker shares it), so the
  ABI stamp of the compiled objects is unchanged.  Python
  (eval/fairscheme.py): failed_successor, and N sets the tag to T_FAIL
  before it dispatches on the tag, so the branch of a failure at the
  cursor runs (boxed_failure, the drop at the root, the rewrite of the
  redex of ($!!)).  The look-ahead holds at every constructor cell the
  scan visits, not at the root alone: ((t ? loop, t), 1) needs it one
  level down.  It does not cross a set guard: a guard whose value is a
  failure is met when the scan enters it, as before (the set-function
  failure path belongs to another lane).  The one-step rule of N and the
  rotation granularity are unchanged; no flag, no default, no change to
  the design of the scheduler, the rotation or the collector.  One order
  of rules changed without a change of values: a failure cell beside a
  choice among the successors of a cell is found before the choice is
  pulled up (N.1 before the pull-tab), so such a configuration is dropped
  without a fork where it forked once and dropped both children before
  (goalOrder below; a choice beside the operation failed, goalChoice, is
  not such a case: the operation is not a failure cell until it is
  stepped).
- The read of the parent cell again after a step (the review of the
  lane).  The read at the extension misses a failure that appears after
  it: a step below the cell can rewrite a shared successor of the cell to
  a failure (the capsule of a set function absorbs the failure as the
  function's own and completes), and one configuration in a queue never
  rotates, so the scan never returns to the root.  captureShared = let t
  = failed in (length (sortValues (set0 (t + 1))), loop, t): the triple
  is read at its extension (no failure yet), the step at successor 0
  rewrites t in place, the scan advances to loop, and without a second
  read it steps loop without end (exit 124 under timeout 60 on both
  backends; the checker reported the step).  Now a local flag of
  procN/N, set by a completed step and cleared by the next descent into
  an operation cell, makes that descent read the parent cell of the
  cursor again (C++: the level of the scan above the cursor, when the
  cursor is not the root level of the scan; Python: state.parent when the
  walk pushed a level).  The root of the scan has no parent in the scan:
  the cell above the inductive position of a step is not read, as a
  failure beside that position does not fail the step (snd (failed, 1)).
  The cost is one read of the parent at most per completed step, bounded
  by the arity; no field of Scan::Level, no change to the one-step rule
  or to the rotation.  captureStep = (after (length (sortValues (set0 (t
  + 1)))), t) with after n = if n == 0 then loop else loop covers the
  step at the cursor itself (the redex becomes loop; the next descent at
  the same cursor reads the pair).  Both end with no value on both
  backends now.  The checker checks the rule the runtime implements: at
  a descent below the root of the scan, the parent cell has no failed
  successor (Checker::descend reads the level above the cursor;
  Checker.descend reads state.parent).  Before the review it read the
  whole spine of the scan, a stronger rule than the runtime kept.
- The checker (both backends).  A hook at the descent of the scan into an
  operation cell (procN, case T_FUNC: Checker::descend; N, the T_FUNC
  branch: Checker.descend) reads the parent cell of the cursor when the
  cursor is below the root of the scan (the C++ walk compares the level
  of the cursor with the root of the scan, so the levels of an inductive
  position or of an enclosing scan are not read; the Python walk reads
  state.parent when the walk pushed a level).  A cell with a failed
  successor is the finding of the descent, kept for the step that
  follows (step_begin of the C++ checker takes it into the step record;
  S of the Python backend takes it with take_descent, so a nested step
  starts with none) and reported when that step completes: "N.1
  (look-ahead)", a completed step at the redex under the constructor cell
  whose successor i is a failure; the configuration has no value and the
  step was not needed.  The count "descents" joins the counts.  A debug
  entry of the bindings, checker_debug_check_step_under(rts, index,
  below=None), puts the scan of the front configuration at a successor of
  its root, or at a successor of that successor, and runs the hooks of
  the descent and of a completed step there.  D15, E5 and E11 are
  unchanged.  The module docstring of checker.py and the checker section
  of DeveloperNotes.rst name the check.
- Tests.  data/curry/FailSib.curry and unit_n1_lookahead.py (both
  backends; one child per class under prlimit and timeout, and one under
  the checker): TestControls (goal0, goalChoice = (1 ? 2, failed) and
  goalOrder = (t, 0) ? (1 ? 2, t), no value; ctrlPair = (1 ? 2, 3), two
  values; ctrlShared, the shared pair of #126, (1, 2)),
  TestDroppedBeforeTheStep, in a child with a 30 s timeout as
  TestPairedCycle of unit_occurs.py (goal, goal2 = ((t ? loop, t), 1),
  goalFwd with t = head [], captureShared and captureStep, no value;
  ctrlOuter = goal ? (5, 5), the value (5, 5) and the end of the run);
  TestSetFunctions, 30 s (goalSet = sortValues (set0 goal), [] on both
  backends, the failure is the function's own and the set is empty;
  goalSetArg = sortValues (set1 (\x -> (x ? loop, x)) failed), a control
  of the boxed-failure path: no value on both backends, and the run
  ended before the change as well, since the first child fails the whole
  set function under 'escape' before the diverging child runs; KiCS2
  ends neither run, so the tests assert the end of the run alone).
  Against the install of the main checkout at b99ae60c (the base of the
  lane) the file fails on both backends with 4 failures of 6 (the
  children of the two classes killed at 30 s); with the change it
  passes, 6 on each backend.  test_step_under_a_failed_sibling of
  unit_py_checker.py (TestAuditChecks) and of unit_cxx_checker.py
  (TestLookAhead), on a constructed state: the pair (id 1, Fwd failed),
  the descent at id 1 and a completed step there are reported, the pair
  (id 1, 2) is not, and the finding is taken once;
  test_step_under_the_parent_alone in both files: the descent at the
  root of a walk reads no parent (Python), ((id 1, 2), Fwd failed) gives
  no finding, ((id 1, Fwd failed), 2) gives the finding one level down.
  FailSib.curry also defines the two limits of the rule, which no test
  runs: captureDeep and guardShared (below).
- The measured cost (the benchmark harness, one run each on the C++
  backend, step mode, the install of the main checkout at b99ae60c as
  "before", the install with the read at the extension alone as "after",
  and the install with the read of the parent after a step as "after2";
  the same Make.config): instructions Peano 2962.0 M, 2837.2 M, 1553.5 M
  (the last run found the compiled products in the cache where the first
  two did not; the steps are equal), Reverse 12458.5 M, 12575.8 M,
  12543.0 M, PermSortPeano 39849.4 M, 41830.1 M, 42070.8 M, TakPeano
  17177.9 M, 17178.1 M, 17177.6 M; CPU seconds 0.580, 0.546, 0.312;
  1.199, 1.144, 1.163; 6.947, 6.905, 6.969; 1.352, 1.322, 1.399; the
  steps are equal on all four in the three runs.  The run-to-run noise of
  one run each on a shared machine covers the CPU differences; the
  instruction counts of Reverse, PermSortPeano and TakPeano are the
  measured cost of the two reads together (at most 5.6 percent, on
  PermSortPeano).
- Verification (the clean environment, CCACHE_DIR set, prlimit, at most
  two test processes at a time).  Before the change, under sprite-exec
  with steps:65536, prlimit 2 GB and timeout 60, on both backends: goal0
  no value, exit 0; goal exit 124.  KiCS2 through the harness (timeout
  60): goal0 ok, no value; goal timeout; goalChoice ok, no value; goal2,
  goalFwd, goalSet and goalSetArg timeout.  With the read at the
  extension alone: captureShared exit 124 on both backends, and under
  checker:true exit 1 with the violation N.1 (look-ahead) at the step at
  loop; goalOrder no value, exit 0.  make stage status 0, make overlay
  status 0 (twice: after the runtime change and after the change of the
  debug entry).  After the change, on both backends: goal, goal2,
  goalChoice, goalFwd, goalOrder, captureShared and captureStep no
  value, exit 0, plain and under checker:true for captureShared; goalSet
  [], exit 0; goalSetArg no value, exit 0; ctrlPair (1, 3) and (2, 3).
  Single files, each ok: unit_n1_lookahead.py 6 on each backend;
  unit_cxx_checker.py 61 on cxx (59 before the lane); unit_py_checker.py
  54 on py (52 before the lane).  Through the runner (./run_tests -j 2
  --mem 4 --backend both): unit_cxx_scheduler, unit_py_evaluation,
  unit_cxx_rotation, unit_setfunctions_semantics, unit_setfunctions_bugs,
  unit_cxx_checker, unit_py_checker, unit_occurs, unit_pairing,
  unit_constraints, unit_n1_lookahead: 22 of 22 run, 0 failed, wall 77.1
  s; unit_cxx_counters, unit_cxx_runtime, unit_cxx_stack,
  unit_cxx_interp, unit_examples, unit_prelude, unit_api,
  unit_cxx_freevars, unit_setfunctions_bindings, unit_goals,
  unit_equality, unit_fingerprint: 24 of 24 run, 0 failed, wall 166.8 s.
  Not run: the whole suite on both backends (the suite stage).
- Changed printed values.  The runs that did not end now end: goal,
  goal2, goalFwd, captureShared and captureStep of FailSib.curry give no
  value; ctrlOuter gives (5, 5) and ends; goalSet gives [].  goalOrder
  and goalSetArg ended before the change as well, with the same result
  (no value).  No other value of the files above changed, and the step
  counts of the four benchmark programs are equal.
- Left to the owner.  (1) A bug found on the way, outside this lane: the
  failure path of the scan under ($!!) writes the failure over the
  argument cell.  procN, case T_FAIL, returns root->make_failure() when
  the root of the scan is not the root of the configuration, and
  applynf_step (currylib/prelude/apply.cpp) passes the slot of the
  argument, not the redex, so the argument node is rewritten in place; a
  sibling configuration shares it after a fork.  nfShared = let p =
  (failed :: Int, 1) in fst (id $!! p) ? snd p gives no value on the C++
  backend (the install of the main checkout at b99ae60c as well), and 1
  on the Python backend and in KiCS2.  The Python N rewrites the redex
  (var.root).  The shape of issue #126 for ($!!); not changed here.  (2)
  tests/manifest.json has no entry for unit_n1_lookahead.py; a
  calibration run adds it.  (3) The look-ahead does not read through a
  set guard.  The reproducer, guardShared = let t = failed in sortValues
  (set1 (\x -> (loop, x)) t) ? [(t, t)]: the first child runs the capsule
  over (loop, guard t); the rotation lets the second child step t to a
  failure and drop; back in the capsule, successor 1 of the pair is a
  guard whose target is the failure, failed_successor follows forward
  cells alone, and the scan enters loop again (exit 124 under timeout 60
  on both backends, plain and under the checker, which uses the same
  helper).  At the cursor the same failure takes the capsule's failure
  path (failure_escapes sees the guard level; fail_capsule).  A read
  through the guard needs the set id of the guard in failure_escapes and
  fail_capsule, the path of the set-function lane; a note on issue #132
  or a new issue for that lane is for the owner to post.  (4) The read
  of the parent after a step does not reach a cell above the parent.
  captureDeep = let t = failed in ((length (sortValues (set0 (t + 1))),
  loop), t): the step at length rewrites t, the scan advances within the
  inner pair, whose read finds nothing, and the outer pair is not read
  again (exit 124 under timeout 30 on both backends; the checker reports
  nothing, as it checks the parent alone).  A read of every level of the
  scan after each step is unbounded (a long list makes a deep scan), and
  a return to the root at every step is the one-step rule, which the
  design excludes; a decision is the owner's.  (5) The values of PAKCS
  for the goals were not observed.  (6) The whole suite on both backends
  (the suite stage).
- The merge of py314 into the lane (2026-10-12), after the set-function
  lane above landed there.  The finding of the look-ahead takes the
  failure path of that lane: in procN and in N the tag T_FAIL lands in
  the case T_FAIL, which calls record_failure (an inside or an outside
  failure recorded on the queue of the capsule) and then returns T_FAIL
  to procD, which drops the configuration, or makes the redex of ($!!) a
  failure; failure_escapes, fail_capsule, boxed_failure and the status
  E_SETFAIL named above went with that lane.  The guards crossed are
  those of the levels of the scan, as for a failure at the cursor: the
  failed successor is one level below the cursor, read through forward
  cells alone, so no guard lies between them.  The checker holds both
  sets of hooks (failure_recorded, capsule_end, descend) and the bindings
  both debug entries.  Item (3) above stands under the new names: a read
  through a set guard needs the set of the guard in record_failure.
- The tests after the merge (2026-10-12).  goalSetArg of
  unit_n1_lookahead.py, the control of the boxed-failure path, does not
  end under the deferred rule (exit 124 under timeout 60 on both
  backends, plain and under the checker; the two tests of TestSetFunctions
  failed on both backends through the runner): the first child meets the
  boxed failure at its cursor, the capsule records an outside failure of
  level zero and goes on with the second child, (loop, guard failed),
  whose failure the look-ahead does not read through the guard (item (3)
  above); before the deferred rule the first boxed failure failed the
  whole set function at once under 'escape', and the run ended.  The
  goal moved to its own class, TestBoxedFailureBesideALoop, whose two
  tests are expected failures until the owner decides item (3); goalSet
  keeps its two tests ([] on both backends).  The comment of
  FailSib.curry says the same.  The other hand runs after the merge, on
  both backends, plain and under checker:true: goal no value, exit 0;
  goalSet [], exit 0; t3 and level of SetFunctionsFailures.curry False
  and [].
- The second merge of py314 into the lane (2026-10-12), after the D26 lane
  (the equality guard at the merge of two bound classes) landed there.
  Three conflicts, all textual: the includes of checker.cpp
  (cyrt/currylib/prelude.hpp of D26 and cyrt/fairscheme.hpp of the
  look-ahead, both kept), the members of Checker.__init__ in checker.py
  (descent of the look-ahead, constraint_bindings and merge_guards of
  D26, all kept), and this file (the D26 entry before this entry).  The
  rest merged by itself: the checker holds the hooks of the three lanes
  (failure_recorded, capsule_end, descend, constrain_begin, merge_guard,
  unguarded_merge), D of fairscheme.py wraps the value of a constraint
  step in the guard cells and N keeps the look-ahead block, and both
  checker test files keep every test.

2026-10-12: rule N.1 of the Fair Scheme read at the cursor (decision D27; worktree wt/n1cursor)
- The decision.  The owner took decision D27 on 2026-10-12: rule N.1 of
  procedure N is read at the cursor of the depth-first scan, as Sprite
  read it before 2026-10-12 and as KiCS2 reads it, not over the whole
  constructor cell as the thesis states it.  The look-ahead of the entry
  above (issue #132, worktree wt/lookahead, merged the same day) is
  removed from both backends.  The reasons: the criteria are a formally
  proven correct and optimal strategy and the best performance against the
  competing implementations; the look-ahead is a cost on every constructor
  visit (and one more read after every completed step) for a benefit that
  arrives only through sharing, and no value depends on it.  A
  configuration with a failure under its constructor spine has no value
  under either reading; a step at the sibling of a failure is a step at a
  needed position; the theorems of safety and optimality hold under both.
  The difference is in termination alone: let t = failed in (t ? loop, t)
  ends under the eager reading and does not end under the cursor reading,
  as in KiCS2.  Issues #132 and #136 close as not bugs after this lane
  merges (the owner's agent posts the closures).
- The rule in the runtime (both backends, the same rule).  The scan of a
  constructor-rooted configuration reads a failure at its cursor alone
  (procN, case T_FAIL; N, the T_FAIL branch): the failure takes the path
  of the deferred failure rule (record_failure, then the drop of the
  configuration by procD, or the rewrite of the redex of ($!!)).  A
  constructor cell at the cursor is extended without a read of its
  successors (the default case of procN; the T_CTOR branch of N), and a
  step is followed by no read of the parent cell.  procN and N read as at
  9ca9e0d7 (the deferred failure rule, before the look-ahead), with one
  comment at the T_FAIL case that names the decision.  The one-step rule,
  the rotation, the record of the deferred failure rule, the guard of D26
  at the merge of two bound classes, the pairing of a decided variable
  (D25), the occurs check and the other checks of the checker are
  unchanged.  No flag, no default, no change of values.
- Removed, file by file.  src/cyrt/fairscheme.cpp: the include of
  cyrt/fairscheme.hpp, the flag stepped of procN, the read of the parent
  cell and the call Checker::descend in the case T_FUNC, and the read of
  the successors in the default case.  src/cyrt/fairscheme.hpp: the file
  (failed_successor), which no other file used (git rm).
  src/cyrt/checker.hpp and checker.cpp: Checker::descend, the fields cell
  and index of the StepRecord, the members descent_cell and
  descent_index, the count "descents", the check "N.1 (look-ahead)" of
  step_end, and the include of cyrt/fairscheme.hpp.
  src/python/backends/cxx/cyrtbindings/checker.cpp: the debug entry
  checker_debug_check_step_under.  src/python/backends/py/eval/
  fairscheme.py: failed_successor, the flag stepped of N, the block that
  set the tag to T_FAIL on a failed successor of the cursor or of the
  parent after a step, the call rts.checker.descend, and the finding S
  passed to checker.step.  src/python/backends/py/eval/checker.py:
  descend, take_descent, the member descent, the parameter unneeded of
  step with its violation, the count 'descents', and the bullet of the
  module docstring.  docs/source/DeveloperNotes.rst: the bullet of the
  check.  The checker files, the bindings and the documentation are
  identical to py314 before the merge of the lane (8ac0dea3).  The ABI
  stamp of the compiled objects is unchanged (the stamp of Prelude.so
  before and after make stage: the same digest), as the two headers were
  outside the include closure of cyrt/cyrt.hpp.
- Tests.  tests/unit_n1_lookahead.py is renamed tests/unit_n1_cursor.py
  (git mv), with the module and class docstrings rewritten for D27.  Kept,
  in TestCursorReading, plain and under checker:true, in a child under
  prlimit and timeout: goal0, goalChoice and goalOrder (no value),
  ctrlPair ((1, 3), (2, 3)) and ctrlShared ((1, 2)), the goals of
  FailSib.curry that end under the cursor reading.  Dropped: the
  end-of-run tests of goal, goal2, goalFwd, captureShared, captureStep
  (TestDroppedBeforeTheStep), goalSet (TestSetFunctions), goalSetArg
  (TestBoxedFailureBesideALoop, the two expected failures), and ctrlOuter,
  whose run does not end under the cursor reading either (the value
  (5, 5) is printed, exit 124 under timeout 60 on both backends).  The
  constructed-state tests of the look-ahead are dropped:
  TestLookAhead of unit_cxx_checker.py (test_step_under_a_failed_sibling,
  test_step_under_the_parent_alone) and the two tests of the same names
  in TestAuditChecks of unit_py_checker.py; both files are identical to
  py314 before the lane.  The goals that diverge stay in FailSib.curry
  under the heading "Records: runs that the eager N.1 of the thesis ends
  and the cursor reading of decision D27 does not, as KiCS2; not run by
  the tests": goal, goal2, goalFwd, ctrlOuter, captureShared, captureStep,
  goalSet, goalSetArg, and the two limits of the look-ahead, captureDeep
  and guardShared, which ended under neither reading.  tests/manifest.json
  has no entry for unit_n1_cursor.py (nor had it one for the old name); a
  calibration run adds it.
- Verification (the clean environment, CCACHE_DIR set, prlimit, at most
  two test processes at a time).  Before the change, under sprite-exec
  with steps:65536, prlimit 2 GB and timeout 60, on both backends: goal0
  no value, exit 0; goal no value, exit 0; goalSet [], exit 0; ctrlPair
  (1, 3) (2, 3); t3 and level of SetFunctionsFailures.curry False and [];
  eqShapeG18 of Constraints.curry (0, 0) (2, 2).  make stage status 0,
  make overlay status 0.  After the change, on both backends, under the
  same settings: goal0, goalChoice, goalOrder no value, exit 0; ctrlPair
  (1, 3) (2, 3); ctrlShared (1, 2); goal, goal2, goalFwd, goalSet,
  goalSetArg, captureShared, captureStep, captureDeep, guardShared exit
  124 with no output; ctrlOuter (5, 5) and exit 124; t3 False; level [];
  eqShapeG18 (0, 0) (2, 2).  Single files, each ok: unit_n1_cursor.py 2
  on each backend; unit_cxx_checker.py 65 on cxx (67 before the removal);
  unit_py_checker.py 57 on py (59 before).  Through the runner
  (./run_tests -j 2 --mem 4 --backend both): unit_n1_cursor,
  unit_cxx_checker, unit_py_checker, unit_cxx_scheduler,
  unit_cxx_rotation, unit_py_evaluation, unit_setfunctions_failures,
  unit_setfunctions_semantics, unit_boundmerge, unit_pairing, unit_occurs,
  unit_constraints: 24 of 24 run, 0 failed, wall 69.1 s.  The pinned
  counters of unit_cxx_rotation.py (psort 8: 16929 steps, 1636 forks;
  countQueens 8: 2112920 steps, 511384 forks) pass unchanged: the
  look-ahead changed no step count.  Not run: the whole suite on both
  backends (the suite stage).
- Changed printed values.  None.  The runs the look-ahead ended do not
  end now (the records above), as before 2026-10-12 and as in KiCS2.
- The staged install.  make stage links each exported header into
  include/cyrt/ and prunes nothing, so the link to the removed
  src/cyrt/fairscheme.hpp dangled after the removal; it was deleted by
  hand in the staged install of the worktree.  After the merge, the
  staged install of the main checkout holds the same dangling link
  (install/include/cyrt/fairscheme.hpp) until it is deleted by hand.
- The rename.  The rewrite of tests/unit_n1_cursor.py kept less than
  half of the text of tests/unit_n1_lookahead.py, below the similarity
  threshold of git, so git log --follow may not join the two names.  The
  commit message should say that the file replaces unit_n1_lookahead.py.
- Left to the owner.  (1) The closures of #132 and #136 as not bugs.
  (2) Item (1) of the entry above, the failure path of the scan under
  ($!!) that writes the failure over the argument cell (nfShared), stands
  as before: it is outside this lane and the look-ahead lane alike.
  (3) The whole suite on both backends (the suite stage).  (4) The
  deletion of the dangling header link in the main install after the
  merge.

2026-10-12: the captured variables of set0 are arguments of the capsule (decision D28; worktree wt/set0lift)
- The decision.  The owner took decision D28 on 2026-10-12: the captured
  variables of the expression of set0, and of a function value given to
  a set function, are arguments of the capsule, boxed at the entry, as
  the arguments of setk are.  The authority is the paper on weakly
  encapsulated search (Christiansen, Hanus, Reck and Seidel, PPDP 2013,
  section 3.3): let x = coin in allValues x yields {0} and {1}, one set
  per outside alternative, and let x = failedBool in allValues x fails
  instead of yielding a set; the implementations (KiCS2, PAKCS), which
  scope by the time of the first evaluation, are not the authority.  The
  research note (revision 3, def:s-entry) reads: set0 e with the
  captured variables v_1 .. v_p is the application of the set function
  of the lifted operation e' = \v_1 .. v_p -> e to v_1 .. v_p, the v_j
  boxed as value arguments; p = 0 is the closed case, unchanged.  The
  reasons: the runtime already reaches that entry state for setk and for
  a function value whose partial application holds arguments (set_step
  and set_ box every argument the function value holds, issue #117), so
  set1 (L x_1 .. x_(k-1)) x_k and setk L x_1 .. x_k reach one entry
  state for any k, and the lifting is a pass of the compiler: no change
  to the C++ runtime, the evaluation of the Python backend, the Curry
  text of the library, the ABI or a default.
- The pass.  src/python/toolchain/flat2icurry/setlift.py, beside
  bindingopt, with transform_prog(prog) -> (prog, count), optimize_set0
  on a program in memory and optimize_file in place, as bindingopt has
  them.  For every saturated call set0 e in a rule of the module (the
  lambdas f._#lambdaN of the front end, the bindings and the bodies of
  lets, the branches of cases and the alternatives of a choice
  included), bottom-up (a set0 inside e first), with the captured
  variables x_1 .. x_k of e (the variables e does not bind, the Curry
  unboundVars, in the order of their first occurrence; a class
  dictionary and a free variable of an enclosing where are captured
  like any other), k >= 1: a new Private function L x_1 .. x_k = e,
  named f_SET0n (f the local name of the enclosing function, n a counter
  per enclosing function from zero; a name an original function has is
  skipped; the front end writes no such name, and the case lifting of
  the translation, which runs after it over the lifted program, skips
  the names the program holds), with a type annotation at the root of e
  removed (the defect of icurry 3.1.0 on a root annotation over a let,
  a free declaration or a case) and the type forall a_0 .. a_k. a_0 ->
  .. -> a_k, a ForallType over k + 1 type variables as the front end
  writes a scheme (the ICurry compiler ignores the type; the file is on
  disk, and the oracle may read it).  The call becomes setk L x_1 .. x_k
  for k up to seven, the arities the library has, and set1
  (L x_1 .. x_(k-1)) x_k beyond.  set0 $ e is the call set0 e (the $ on
  the partial application set0 is reduced as bindingopt reduces it).  A
  closed set0 (k = 0) is unchanged.  The pass is idempotent:
  transform_prog over its own output changes nothing, count 0.  The
  interface M.fint is never written: the lifted function is private,
  and the importers number the public functions of a module from that
  file.
- The plumbing.  flat2icurry.rewrite_prog applies the two passes in
  order, the binding optimization then the lift, and returns the pair
  of counts; rewrite_file writes the file once when either changed it,
  in the format of the front end, else leaves it with its time.
  _frontend.optimize_flatcurry calls rewrite_file over the module's
  .fcy and every .fcy the front end wrote, so both routes, the port and
  the external icurry, read the lifted file.  rewrite_on_hit's early
  return and its marker cover set0 (SET0_NAME, the bytes of the
  qualified name in the .fcy).  translated_before_rewrite has one rule
  per pass: a .fcy that holds a saturated set0 call whose argument has
  captured variables, beside an .icy that lacks the functions the lift
  would make (the names come from the pass in memory, memoized by the
  size and the time of the file), is stale, and so is the .icy; the
  library is left out as before (no library module uses set0).
  ROUTE_VERSION is 2, with a dated line in its comment, pinned by
  unit_curry2icurry_stale.py and unit_setlift.py; setlift.py and
  __init__.py of flat2icurry join REWRITE_SOURCES, the digest of the
  icurry route.  One flag for both passes: translate(..., rewrite=True)
  replaces bindingopt=True, --rewrite replaces --bindingopt of
  python -m curry.toolchain.flat2icurry, and
  python -m curry.toolchain.flat2icurry.rewrite and
  sprite-make --rewrite-flat run both.  One flag, because the step is
  one unit on the routes: the file on disk holds both rewrites or
  neither, and a caller that wants the ICurry the routes make wants
  both; a caller that needs the text of the front end passes
  rewrite=False as before.  The oracle's accept_unrewritten runs
  rewrite_prog in memory, and the idempotence test of
  func_flat2icurry.py covers both passes over the archive.
- Tests.  New: tests/unit_setlift.py with tests/data/curry/SetLift.curry,
  both backends, in a child under prlimit and timeout, plain and under
  the checker.  TestPass, the pass on FlatCurry terms (15 tests: the
  captured variables and their order, the type, the forms of the call,
  the names, one and two variables, the closed and the unsaturated
  cases, set0 $ e, the root annotation, the nested set0, every kind of
  rule, eight and seven variables, a name collision, an external rule,
  the step of the routes).  TestGoals, the parity corpus: each program
  point set0 e with captured variables beside its hand-written lifting
  setk L xs, with the same values: one variable (one), two (two), a
  class dictionary (dictS, show over a Show-constrained parameter), a
  free variable of the enclosing where (freeS, one set per binding as
  s6PG and capNarrow), a free variable declared inside the expression
  (innerFree, closed), seven variables (set7) and nine (set1 over a
  partial application of eight), a nested set0, a set0 inside a lambda,
  set0 $ e (dollar, dollar2), a root annotation (typed), set0 in the
  branches of a case and under a let, the wrapper of the paper applied
  to a choice (wrapc: [0] and [1]), a closed set0 and the limit of the
  lift (partial: map (sortValues . set0)).  TestPinnedValues: heap1 no
  value, heap2 [0] and [1], f0cap no value, s0idf [], s0f [], s1f1 no
  value, s0c [0, 1], s1idc [0] and [1], wrapc [0] and [1].
  TestChecker: a closed set0 is a capsule without a box (the count
  capsules_without_box of the C++ checker), a lifted set0 has its box.
  TestFiles: the pass over the FlatCurry file of the module (19 lifted
  functions, private, of the polymorphic type, the root annotation
  stripped), optimize_file and rewrite_file (the file written once, the
  interface files untouched by content and time), the command lines, a
  pair made before the lift found stale and translated again, the hit
  of the ICurry cache lifting the file, and ROUTE_VERSION 2.  Moved:
  captureShared, captureStep and captureDeep of FailSib.curry from the
  records to the tests (TestCapturedFailure of unit_n1_cursor.py, no
  value, plain and under the checker; their comments rewritten: the
  boxed t fails as an outside failure of the set function, which fails,
  so length fails at the cursor before the scan reaches loop);
  guardShared stays a record, with its comment adjusted (the boxed
  failure sits behind loop inside the capsule, which never reaches it).
  Kept with their values: set0Inner (no value) and set0InnerV ([[7]]) of
  unit_setfunctions_failures.py, with the comments rewritten: a is a
  captured variable of the inner set0, now boxed under the inner guard
  over the outer one; the inner capsule meets the failure as an outside
  failure, has no value and no inside failure, so it fails, and the
  failure goes under the outer guard, where the outer capsule fails in
  turn: no value, as before, when the inner capsule met the failure
  through the outer guard alone.  Dropped: none.  The comment "set0
  boxes nothing" of SetFunctionsFailures.curry is corrected, and exprS
  of the Python backend gets a comment that says why no captured
  variable reaches it.  unit_curry2icurry.py gains test_set0_lift_icurry:
  the icurry route reads the lifted file and writes the same .icy as the
  port for SetLift, and the oracle harness finds the pair equal.
  tests/manifest.json has no entry for unit_setlift.py; a calibration run
  adds it.
- Documentation.  AGENTS.md (the paragraph on the route: two rewrite
  passes, the stale rule), docs/source/CurryPrimer/Semantics.rst (the
  paragraph on the captured variables of set0, with main8 to main10),
  docs/source/Introduction/CompilationPipeline.rst (the lift, the entry
  rule, the limits, the stale rule), tests/README (the step, the stale
  rules, accept_unrewritten, the test files).
- Limits.  A set0 that is not a saturated call is left alone: a partial
  application (map set0 xs, sortValues . set0) or a set0 reached through
  a variable; the pass sees no argument, and the element reaches set0 at
  run time as an encapsulated expression (the goal partial of
  SetLift.curry pins [[0, 1]]).  The type of a lifted function is a
  fresh polymorphic scheme, not the type of e: no reader in Sprite types
  a FlatCurry file, and the oracle compares ICurry, which has no types.
  The check on an import parses a .fcy that names set0 even when its
  calls are closed (a parse, once per process by the size and time of
  the file), where before only an equality name without constrEq cost
  a parse.  The ICurry cache of every tree misses once under version 2
  of the route.
- Verification (the clean environment, CCACHE_DIR set, prlimit, timeout,
  at most two test processes at a time).  Before the change (the probes
  at the start of the lane, under sprite-exec with steps:65536, prlimit
  2 GB and timeout 60, the same values on both backends): heap1 [], exit
  0; heap2 [0, 1]; f0cap []; s0idf []; s0f []; s1f1 no value; s0c
  [0, 1]; s1idc [0] and [1]; wrapc [0, 1]; captureShared, captureStep
  and captureDeep of FailSib.curry exit 124 with no output.  After the
  change, on both backends, plain and under checker:true, under the same
  settings: heap1 no value, exit 0; heap2 [0] and [1]; f0cap no value;
  s0idf []; s0f []; s1f1 no value; s0c [0, 1]; s1idc [0] and [1]; wrapc
  [0] and [1]; captureShared, captureStep and captureDeep no value, exit
  0.  The first import of FailSib and of SetFunctionsFailures found
  their pairs stale under the lift rule and translated them again (the
  .fcy and the .icy hold captureShared_SET00, captureStep_SET00,
  captureDeep_SET00, set0Inner._#lambda10_SET00 and
  set0InnerV._#lambda11_SET00); the corpus holds 24 lifted functions in
  three modules (SetLift 19, FailSib 3, SetFunctionsFailures 2), and no
  other module of the corpus has a set0 over captured variables
  (StackGuard, CxxPartial, FunPatFreeArgSet, basic00, basic01 and the
  Poker benchmarks use closed set0).  make stage status 0; the ABI stamp
  of Prelude.so before and after: the same digest.  The
  prepare pass (./run_tests --prepare-only --backend both -j 2 --mem 4):
  1252 modules in 12 directories, 24 of 24 ok, 0 failed, wall 580 s,
  every product present; it translated 2 pre-rewrite pairs again, one
  under data/curry (SetFunctionsFailures, the lift rule) and one under
  data/curry/smap (the binding rule, a pair of the overlay); the C++
  side compiled the objects of a cold tree (eqconstr alone 327 s).
  Through the runner (./run_tests -j 2 --mem 4 --backend both), the
  touched files, the files with closed set0 uses and the files of the
  renamed flag: unit_setlift, unit_n1_cursor, unit_setfunctions_failures,
  unit_setfunctions_semantics, unit_setfunctions_bindings,
  unit_setfunctions_bugs, unit_curry2icurry_stale, unit_sigtable,
  unit_cxx_checker, unit_py_checker, func_flat2icurry, func_setfunctions,
  unit_flat2icurry_bindingopt, unit_curry2icurry, unit_cxx_partial,
  unit_py_fairness, unit_cxx_stack and unit_funpat_free_argument: 36 of
  36 run, 4 failed in the first pass (wall 193 s), two defects of the
  lane, corrected and run again.  rewrite_prog handed the lift the
  program the binding optimization rebuilt without a replacement (its $
  reduced), where optimize_bindings hands back the original
  (test_rewrite_is_idempotent of func_flat2icurry.py, both backends):
  rewrite_prog takes the optimized program only when an equality was
  replaced, _rewrite_count of _curry2icurry.py calls rewrite_prog, and
  the parse counts of unit_curry2icurry_stale.py patch the bindingopt
  module, which the step calls.  The parity test of lam compared the
  values in the order written where the child sorts them as text
  (unit_setlift.py, both backends): the expectations are sorted now.
  After the corrections, on both backends: func_flat2icurry 8 tests,
  unit_setlift 48, unit_flat2icurry_bindingopt 32, unit_curry2icurry_stale
  18 and unit_curry2icurry 22 ok, the last with the new
  test_set0_lift_icurry (the icurry program reads the library through
  CURRYPATH, which the route binds to the path given, so the test passes
  the system path, as config.currypath() of a caller holds it).  Every
  file of the list passed on both backends in its last run.  make -C
  docs html: build succeeded, 2 warnings, both in docstrings of files
  outside the lane (backends/__init__.py and cxx/interface.py); the
  Reference has the page of setlift.  The
  public-safe grep over the changed and the new files prints nothing.
  Not run: func_goal_defaulting.py (the PAKCS REPL oracle does not run
  in the environment of the lane) and the whole suite on both backends.
- Changed printed values.  A captured variable that fails is an
  outside failure: an alternative that demands it is dropped, and the
  set0 has no value when no alternative gives a value and none fails on
  its own, where before the set was empty (heap1, f0cap; the capture
  goals of FailSib.curry, which end now; set0 (w ? 7) with w = failed
  still gives [7], as example (2) of the paper prescribes); a
  set0 over a captured choice gives one set per alternative, where
  before it gave one set of both alternatives (heap2, wrapc, one, two).
  A closed set0 is unchanged.
- Left to the owner.  (1) The commit (the owner's agent commits after
  the suites).  (2) The whole suite on both backends, and
  func_goal_defaulting.py where the PAKCS oracle runs.  (3) A
  calibration entry for unit_setlift.py in tests/manifest.json.  (4) The
  ICurry cache of every tree misses once under version 2 of the route,
  and a tree made before the lift translates its modules with a set0
  over captured variables again at the first import; sprite-make
  --rewrite-flat M does it by hand.  (5) The research note: def:s-entry
  of revision 3 states the rule this lane implements; nothing in the
  note changes here.  (6) The PAKCS oracle reads the lifted .fcy of a
  module with a set0 over captured variables; that PAKCS accepts such
  a file is not verified (no module of the functional corpus has one):
  where the oracle runs, import SetLift under Sprite, load it in pakcs,
  evaluate heap2, and record the result in tests/README section 8.
