Revision history for Proc-Spawner (EXPERIMENTAL)

0.09  2026-11-08
      - Since 0.08: what posix_spawn has no option for (rlimit, sigignore,
        nice: M1); no posix_spawn at all (M2): fork and exec, and where there
        is no fork either, Run 0.07 (repository ilyaz-ai/run; to be released
        first, recommended in META) applies the redirections around Perl's
        own spawn; Windows: the kept process handle released, sigignore of
        PIPE and INT, t/windows.t; ulimit through getrlimit; X29 fixed, X31
        fixed, X32 recorded; known issue X27 classified; TODO; the documents
        cross-linked.  The figures of post4-0.04 hold (the measured files
        differ only in $VERSION); digest of the code they depend on:
        80e40b1ce413ea4c39203a0c5bbd134b800477da8641dca26dc55aa01e693d41.
      - Known issue X32 (ours; possible, not observed): without /proc, Perl's
        own writes into a pipe the caller gave are blocking writes of
        PIPE_BUF bytes after select, and block if a child fills the pipe in
        between.  In the known issues, Proc::Spawner's LIMITATIONS, README
        and TODO.
      - The Markdown documents link to each other and to the files they name
        (398 links, relative; none broken): a path or file name in a code
        span became a link, a script followed by its arguments links its
        name.  Mentions of the old Project's files link to the branch
        project-2026-11-03 where they are there (19, checked through the
        GitHub API; the repository is private, so they work for those who
        can see it).  doc/perl-win32-system-bugs.md now ships (a shipped
        document links to it), so every relative link of a shipped document
        points into the distribution.
      - M1 (the user, 2026-11-06: what posix_spawn has no option for, done
        around the call where it can be undone, failed for the rest): new
        settings sigignore (signals the child ignores), rlimit (its resource
        limits) and nice (an increment), in start and spawn.  The XSUB sets
        them in this process right around its call, in C, with every signal
        blocked, and puts them back right after.  Unsupported (dropped or
        failed with ENOTSUP, as UNSUPPORTED says): a hard limit lower than
        ours without CAP_SYS_RESOURCE, a higher nice value without the right
        to lower it back, a CPU limit this process has used up, ignoring
        SIGCHLD; all three on Windows.  Checked as root (here without
        CAP_SYS_RESOURCE) and as an unprivileged user; t/spawner.t.  A memory
        limit below what this process uses makes glibc's posix_spawn fail
        with ENOMEM (documented).
      - M2 (the user, 2026-11-06: no posix_spawn at all, as Run 0.06's
        forkless mode): Makefile.PL probes posix_spawn itself; without it,
        or with PROC_SPAWNER_MISSING=posix_spawn, the primitive is the new
        XSUB __spawn_nowait, fork and exec in C with nothing in between, and
        every other setting is done around the call.  The whole suite passes
        so, and a build without posix_spawn passes t/spawner.t and t/dash.t.
        t/missing-matrix.pl: profile "no posix_spawn"; the Windows profile
        has the M1 settings.
      - Stale since 0.07: "the shell layer needs posix_spawn's chdir"
        (README, X10, t/dash.t).  chdir is done around the call where the
        libc lacks it; checked by building without chdir and fchdir: t/dash.t
        passes, 13 of 13.
      - M2, no fork either (the user, 2026-11-07): the redirections are
        delegated to Run 0.07 (repository ilyaz-ai/run; to be released
        first), whose start() applies them around Perl's own spawn, system {
        FILE } 1, ARGV (P_NOWAIT), as on OS/2, Win32 and VMS, and puts our
        fds back ($Proc::Spawner::PRIMITIVE 'system1'; chdir and fchdir are
        done here).  PROC_SPAWNER_MISSING="posix_spawn fork" selects it on
        Linux too, where Run emulates the spawn by fork and exec: the whole
        suite passes so.  The arguments go to Perl as they are: Perl's command
        line on Windows does not quote them as the C runtime parses them back
        (❰BUG❱ in Perl; doc/perl-win32-system-bugs.md, to be reported), so the
        tests of exact argv and of batch files are TODO there (the user: no
        bug-for-bug workaround).  The program is looked up here (posix_spawn's
        meaning of FILE and search).  The child sees fds 0-2 only (dup2 or
        open onto fd 3 and up unsupported); pidfd, a non-empty sigmask,
        fchdir, a group to join unsupported; at most 64 children (EAGAIN).
        New t/windows.t (Windows only): both primitives under Wine with
        Strawberry Perl 5.38.2, 13 passing, and the 2 TODO with system1.
        OS/2 and VMS not tested.  A Run that is missing or too old is now an
        error of spawn (before: any exception inside became EINVAL).
      - Windows, both primitives: sigignore of PIPE (no SIGPIPE there) and of
        INT in a new process group (Ctrl-C is off there) need nothing; other
        signals stay unsupported (Perl's kill ends a Windows child, or sends
        Ctrl-Break, which it cannot ignore).
      - ulimit (the shell layer) reads the limits with getrlimit, through
        Proc::Spawner's XSUB, instead of /proc/self/limits or, without /proc,
        asking a shell (the user, 2026-11-07: the module has XS anyway).  Same
        output as dash for -n -Hn -f -s -c -t -v -l -m -d; t/dash.t passes
        with /proc hidden too.
      - X29 ❰BUG❱ (ours; 0.07, 0.08): where the fd actions are done around the
        call, the copies of our fds went above an open action's flags plus
        1000, beyond the common limit of 1024 fds, and the call failed with
        EINVAL; now just above the highest fd the actions name (the 1000 was
        never needed).  Found on Windows; checked on Linux with ulimit -n 1024.
      - Windows: __dup_high (a copy of an fd above a number) works there.
      - Windows: the process handle kept for a child's exit code is released
        once the loop has the status (Proc::Spawner::reaped, also for callers
        of the raw spawn who wait themselves).  Checked under Wine with
        Strawberry Perl 5.38.2: the handle is gone after the status (0.08:
        still open); 1,100 children in a row give their status.
      - X31 (ours; only in the Windows code, which no release builds yet):
        Proc::Spawner's open action on Windows (win_spawn.h) did not map
        /dev/null to NUL, as Perl's own open does.  Fixed; checked under Wine
        from a directory on C: (on Wine's Z:, the Linux root, the bug hides):
        a test program of win_spawn.h failed before and passes after, and
        t/windows.t (new test) passes with both primitives.  Perl's other
        Windows special cases checked against ours: nothing else missing.
      - X11 and X12 closed (the user, 2026-11-07): the Windows primitive
        exists (win_spawn.h; M2's system1), and the corpora were run through
        the executor by the run-time comparison of post4-0.04.
      - TODO: every open thing in one place, by who has to act (the
        decisions waiting for the author first); linked from README.
      - Known issue X27 classified (the user's method, 2026-11-07: parse
        all the samples in one batch, split them into atomic blocks marked
        with their samples, and pass only what is shown unaffected, so that
        a pass is definite).  New doc/large-subset/tools/
        large-subset-blocks.pl sorts the 40,410 accepted samples from the
        parse trees alone, programs taken as real and opaque, into strict
        (365: nothing in the environment can change the comparison, given
        only how the shell is started), held (2: the same, under assumptions
        known to hold in every tested environment, such as /dev/null),
        almost strict (34,291: the same under named assumptions, mostly that
        a program does the same in each run and the shells look it up
        alike) and undecided (5,752,
        each with the codes of what it may depend on: files, variables, a
        program's output or status taken by the shell, /tmp, ...).  $? takes
        the status of what ran before it, set -e makes every later status
        decide (the user, 2026-11-07); absolute paths are told apart by the
        area they are in (path-tmp, path-system, ...), and whether the run
        could control it; /dev/null is an assumption known to hold in every
        environment tested (the level "held": dash and bash have it, ours on
        Windows gets NUL, X31).  The record,
        one line per sample by snippet ID with a short code, is
        doc/large-subset/large-subset-provenance.tsv; the codes, how to
        remake it and the findings: doc/large-subset/
        large-subset-environment.md.  Checked by 84 probes, by reading all
        strict samples and random others, and against the run's
        differences.  It replaces the first version of the same day, which
        tagged what it recognised and passed the rest (81.4 %): its passes
        were not definite.
      - The batch check (-batch): the samples joined into one text parse as
        they do alone, but for the 198 that change how the text after them
        parses (functions, aliases, P1, a trailing backslash, exec) and 2
        whose arithmetic text keeps its trailing blank (large grammar) or not
        (base grammar), same value.  New known issue X30 (ours): with $LINENO
        in a text, the parse time grows with the square of its length (the
        joined text: 89 s, 8.4 s without the one $LINENO).

0.08  2026-11-06
      - Since 0.07: Windows (EXPERIMENTAL): win_spawn.h compared with Perl's
        own spawn and fixed; the module built with MinGW-w64 against
        Strawberry Perl 5.38.2 and run under Wine; batch files; the options of
        %Proc::Spawner::WINDOWS.  Fixed on every system: spawn's result in
        scalar context when a setting is missing.  The figures of post4-0.04
        hold (the measured files differ only in $VERSION); digest of the code
        they depend on: 3956c46c16d3290baf27f9475da8b72d431f17a194b2ba9fba91e3e0fe863bb8.
      - Windows, run under Wine with Strawberry Perl 5.38.2 (Spawner.dll built
        with MinGW-w64): spawn, waitpid, options work.  New: batch files run
        through the system's cmd.exe, arguments quoted as Rust's standard
        library does since BatBadBut (CR, LF refused); %Proc::Spawner::WINDOWS:
        new_group (Ctrl-C reaches the parent, not the children; default on),
        kill_with_parent (a job object that ends the children with the parent;
        default on), new_console (as Perl's spawn; default on); console
        pseudo-handles of Windows 7 kept out of the handle list.  Fixed: spawn
        returned its count of results in scalar context when a setting was
        missing (now the pid); Proc::Spawner no longer uses fcntl and SIGCHLD
        on Windows (the loop polls there).  doc/pspawn-spawner.md, section 8:
        how the child sees its arguments, Perl's system against ours.
      - win_spawn.h compared with Perl's own spawn on Windows (win32/win32.c of
        perl 5.38.2) and fixed: PATHEXT's extensions before the bare name, and
        quoted PATH entries, as Perl's qualified_path(); cmd.exe /c quoted as
        Perl's create_command_line() does; the environment block sorted, with
        the drives' directories (=C:) kept; E2BIG over 32766 characters;
        strings not held as UTF-8 read in the ANSI code page (as Perl's
        CreateProcessA); the default environment and directory are Perl's
        (PerlEnv_get_childenv, PerlEnv_get_childdir); its own allocator on the
        process heap (found by compiling against Strawberry Perl 5.38.2, where
        malloc and free are Perl's macros).  Spawner.dll compiles and links
        with MinGW-w64 against Strawberry Perl's headers and libperl538.a; not
        yet loaded.  Open questions and the deliberate differences:
        doc/pspawn-spawner.md, section 8.

0.07  2026-11-05
      - Since 0.06: what the spawn primitive lacks is done around the call
        ($Proc::Spawner::MISSING; t/missing-matrix.pl); win_spawn.h, posix_spawn
        for Windows (not yet built into the module); Z0 (printf's 0 flag with
        %s); the atomic calls measured.  The figures of post4-0.04 hold (the
        measured files differ only in $VERSION); digest of the code they
        depend on: 3f1c152adeabd6730535ae4380824c41e45c896190d612e75e08f956c15af270.
      - win_spawn.h: posix_spawn for Windows on CreateProcessW (the user,
        2026-11-05; EXPERIMENTAL, not yet built into the module: that needs a
        Windows Perl).  Spawner.xs includes it under _WIN32 and #defines the
        posix_spawn names to it (#define posix_spawn win_posix_spawn and so
        on), so __posix_spawn is one source; no SIGCHLD helpers there.  The
        child's fds 0-2 through STARTUPINFO, fds 3 and up through the C
        runtime's lpReserved2 table, exactly the handles it should have
        through PROC_THREAD_ATTRIBUTE_HANDLE_LIST (inheritable copies made for
        the call); the file actions in order (open by the parent, relative to
        the child's directory; chdir, fchdir, closefrom); argv quoted as the C
        runtime parses it back; posix_spawnp through PATH and PATHEXT;
        pgroup 0 as CREATE_NEW_PROCESS_GROUP; ENOTSUP for a non-empty sigmask,
        a group to join, setsid, sched.  Checked with MinGW-w64 (gcc 13.2)
        under Wine 9.0: argv round trips, fd 3, chdir, dup2, closefrom, a new
        group, the refusals, and the #define layer of Spawner.xs; compiled
        (not linked) for x86, x64 and ARM64 with clang.  Not done yet: the
        Perl side on Windows (waiting for a child through the kept process
        handle, __win_spawn_handle; no SIGCHLD).
      - Proc::Spawner::spawn does what the spawn primitive lacks around the
        call (the user, 2026-11-05: as a Windows port will have to):
        $Proc::Spawner::MISSING, a bitmap of the settings the primitive lacks
        (this libc's gaps, plus PROC_SPAWNER_MISSING for tests).  chdir,
        fchdir, the fd actions (dup2, close, open, closefrom), sigmask,
        sigdefault, pgroup => PGID, resetids, search and pidfd are done in
        this process around the call and undone; setsid, tcsetpgrp,
        pgroup => 0, sched and cgroup are unsupported (drop or fail).  New
        XSUBs __native, __set_cloexec, __dup_high, __fchdir, __pidfd_open,
        __fd_limit.  t/missing-matrix.pl runs the suite once per entry of the
        bitmap, and with the Windows gaps; doc/pspawn-spawner.md, section 8.
        t/spawner.t: a test of pgroup => PGID; pgroup => 0 and setsid are
        skipped where unsupported.
      - Z0 (the user, 2026-11-05): printf's 0 flag with %s, %c or %b (printf
        '%0.s-' 1 2 3, a common way to repeat a character; %05s) is dropped
        before the program printf runs, so it pads with blanks, as glibc's
        printf(3) and both shells; coreutils refuses it ("invalid conversion
        specification").  The flag is undefined there in C and POSIX
        (large-subset-state.md, section 1).
      - large-subset-builtins.pl: a command name starting with - or + is
        quoted, and plain names after export, readonly, unset, local stay bare
        (else the call is no longer the one that ran); -reuse OLD.jsonl.
        Results in large-subset-state.md section 7: of 25,490 atomic calls
        where no program runs, none where dash and bash agree and ours
        differs; the external calls: one (the broken pipe of echo).
        doc/large-subset/tools/large-subset-builtins-differences.tsv.

0.06  2026-11-05
      - Since 0.05, the same day: the fixes for systems without /proc, and the
        measurement of the atomic calls extended.  The figures of post4-0.04
        hold (the measured files differ only in $VERSION); digest of the code
        they depend on:
        c19ac884428cba12a90ff7e8d1bfc4dba7b540e621dc72062563a5ce4ec950b1.
      - RT #182479 (Slaven Rezic: the tests fail on FreeBSD, which has no
        /proc and no /bin/true; thanks to Slaven for the report and for his
        smokers).  The tests find programs through PATH, read
        signal dispositions, masks and process groups from a Perl child
        instead of /proc, and skip what needs Linux's /proc (t/graph.t's
        reporter, the batteries fdbattery and sigenv, pidfd without
        /proc/self/fdinfo).  Running the suite with /proc hidden found three
        faults of the library itself without /proc, now fixed: (1) Perl's
        writes into a pipe it shares with children set O_NONBLOCK on the
        shared open file, so the children's writes failed with EAGAIN and
        output was lost (known issue L10's fallback); a pipe of ours is now a
        named FIFO opened once more for Perl alone, and a pipe we were given
        takes PIPE_BUF bytes per blocking write; (2) the check whether an fd
        is close-on-exec read /proc/self/fdinfo, and took every fd as
        inherited without it (echo x >&5 then failed like the program, not
        like dash): now fcntl, through the new XSUB
        Proc::Spawner::__fd_cloexec; (3) ulimit died ("Undefined subroutine
        Proc::ShellSpawn::Large::unsupported") without /proc/self/limits; it
        now asks a shell, which has Perl's limits: dash, bash --posix, ksh or
        sh, in /bin first, then on PATH (none: "not supported", status 248).
        Known issue X28.
      - The atomic calls where no program runs (large-subset-builtins.pl) now
        include commands not found and settings alone ($ONBUILTIN reports
        them too), with messages as dash's, bash's or neither; the external
        calls of every sample in the classes of the main table
        (large-subset-runreport.pl -x).

0.05  2026-11-05
      - The release of post4-0.04, with the version number 0.05 and
        NOTES-post3-0.04.txt no longer shipped.  The figures of post4-0.04
        hold (the measured files differ only in $VERSION); digest of the code
        they depend on:
        743121ae0b5fb7af55b5308fb51c3db9cf61ca74ec2d3a40e98a3dc6cca634cd.
      - In short, since 0.04: functions before regular builtins (3A); rule-E
        choices; background jobs go on between run() calls (K3); aliases
        known only when they run (P1); rule F ([ a \< b ]); read with names
        from expansions; bracket expressions with quotes, expansions or '/';
        K0 (kill with no process ID); bugs X25, X26 and others fixed; the
        behaviour of every accepted corpus sample compared with dash and bash
        at run time (doc/large-subset/large-subset-state.md, section 7).
      The documents label the unreleased states between 0.04 and 0.05 post-0.04
      to post4-0.04; their entries follow, newest first.

    post4-0.04:
      - Bracket expressions with quoted members, expansions or '/' (the work
        list, large-subset-state.md section 4; both shells agree, probes
        brquote, brslash): grep ^[\>\<], ls [a"]"], ^["$v"], [$v], and
        something[^/]*.  Quoted characters are members, never operators
        (also ! ^ - ]); an unquoted expansion's characters are active.  In
        pathname expansion a bracket expression with a '/' is none: its '['
        is literal, as both shells.  Also in the pattern of ${x%[...]}.  The
        large grammar only; Proc::ShellSubset itself still rejects them.
      - In ${x:-word} (not a pattern), * ? [ are accepted: literal where the
        ${...} is quoted ("${1:-sure? [y/N]}"), active where it is not
        (probe dqpword).
      - Case patterns and ${x%p} are put together whole before they become a
        regex, so a bracket expression may span quoted and unquoted parts.
      - Fixed: [^...] (rule E: negation) was turned into [!...] by the
        parser, so a word that matched no file came out as [!...] instead of
        as written.  The parser now keeps '^'; the executor reads it.
      - Fixed: unset NAME[...] was accepted and did nothing, with status 0;
        dash stops with "bad variable name", bash unsets an array element
        (probe unsetarr).  It is rejected now, as bash's arrays are; known
        issue X26.  New detector feature unsetarr (B); unset foo][ is B by
        hand (probe unsetbad: dash fails, bash returns 0).
      - Proc::ShellSpawn: $ONBUILTIN, a code ref called before each builtin
        or function runs (for measurement; t/try.pl logs to the file named by
        RECBUILTIN); doc/large-subset/tools/large-subset-builtins.pl runs every
        builtin call of the corpus alone in dash, bash and ours;
        large-subset-runreport.pl -x compares the external calls one by one.
      - The behaviour figures in separate tables, with and without a
        coreutils program involved (large-subset-runreport.pl -q), and the
        atomic calls: every call the corpus makes to echo, printf, test, [,
        kill or pwd, run as dash's builtin, bash's builtin and the program
        (large-subset-threeway.pl; the database large-subset-threeway-db.jsonl,
        757 calls; calls made by xargs are left out).  With K0, dash and bash
        agree and ours differs in 7 samples of 40,410 (first run: 57).  The
        messages are compared as multisets of characters (the shells' messages
        of pipeline elements mix).  README: a summary, near the top.
      - The Git history now has the CPAN releases 0.01 to 0.04 (tags v0.01 to
        v0.04); CPAN's 0.04 is the tree once called "the pre-release snapshot".
        doc/pspawn-spawner.md, section 8, and the post-0.04 entry below: the
        Windows section was never in a CPAN release.
      - K0 (the user, 2026-11-05): kill with a signal (-N, -NAME, -s NAME) or
        --, and no process ID (kill $(pgrep x) when nothing matches), is
        answered by us as by both shells: dash's usage text, status 2 (the
        program kill: a long usage, status 1).  large-subset-state.md, section
        1: the rules are a triage of the past; two principles of least
        surprise in conflict (a tool called in a script as when called alone;
        a tool's own treatment of rare edge cases), and the per-tool
        decisions (K0, rule F, Q2).  Known issue X27 (TODO): which samples of
        the run-time comparison depend on the environment.
      - New: the behaviour of the accepted samples, run in dash, bash --posix
        and ours (tools/large-subset-runcompare.pl, large-subset-runreport.pl;
        large-subset-state.md section 7): of 40,410, all three agree in
        15,760, only the messages differ in 23,368, and dash and bash agree
        while ours differs in 33 (29: kill with no process ID, the program's
        status 1 against the shells' 2; question for the user).
      - Fixed, found so: a string with no command, or only comments, was
        rejected by run() (the parser option 'empty' was missing from
        run()'s options), though the measurement counted it as accepted;
        eval of empty text stopped with status 248; kill -s WINCH $$ (also
        CHLD, URG, CONT, kill -0 $$) ended the shell with status 156.
      - Figures: 44 samples rejected within the lower yardstick (post3-0.04:
        49; 3 accepted now, 2 reclassified as blatant by unsetarr); digest
        aa2734f2147be58073d44713de7f003cdee525f593d5e90f70f9f64e900f08bc.
        Tests LS:319-323; battery lines in t/batteries/large-subset-post04.txt.

    post3-0.04:
      - Aliases known only when they run (P1, the user, 2026-11-03: "go with
        better aliases"): where an alias definition has an expansion, or stands
        in a compound command, a function, an and-or list, a pipeline or the
        background, the parser stops after that line; the executor parses the
        rest of the text when it gets there, with the aliases defined by then,
        as both shells read a text one complete command at a time.  Tree node
        { rest => TEXT, lineoff => N }.
      - Rule F (the user, 2026-11-03: where a utility is implemented wrongly,
        we may do better): [ s1 \< s2 ] and [ s1 \> s2 ] (also test, also after
        !), which both shells run and the coreutils program refuses, are
        compared by the executor, by bytes; the option test_lt_gt is needed
        only for longer expressions with < or >.
      - doc/large-subset/large-subset-state.md, section 4: what POSIX says of
        the ${...} forms both shells refuse (an expansion error, or
        unspecified).
      - Figures: 49 samples rejected within the lower yardstick (post2-0.04:
        72); digest
        b189e43efd804af3e15e4224d2e7f46a890d7723301dd792fd9094ec8aaf6c59.

    post2-0.04:
      - read takes names from expansions (read $v, read "$v"), checked when it
        runs, with dash's messages and status 2; a read-only name no longer ends
        the shell (as dash).  ${##}, ${#?}, ${#!}, ${#$} accepted (both shells).
      - Fixed: a name from an expansion in export "$v" was never checked against
        the names the subset may not set (the test was always true).
      - Measurement: 14 samples left out by hand as noise
        (doc/large-subset/tools/large-subset-noise.txt, read by
        large-subset-drops.pl; the user, 2026-11-03); the detector counts the
        decided rows N1-N3 (${x:?}, a special builtin's error, command -v) as
        class X, as large-subset-logical.md says of every decided row.
      - Figures: 72 samples rejected within the lower yardstick (post-0.04: 91);
        digest (now also over large-subset-noise.txt)
        a23490770df33a87e8cbc1008d7d7301c7bcf567cb56bd2d5b5e13a94e0c4636.

    post-0.04:
      - 3A (the user, 2026-11-03): a function comes before a regular builtin of
        its name and before the commands run as programs (POSIX order).  Functions
        may be named cd, read, jobs, echo, kill, pwd, ...; such calls are checked
        at run time (Proc::ShellSpawn::Large::b_late); command NAME reaches the
        regular builtins we run.  Not alias, unalias.
      - Rule E choices done: ${@#p}, ${*%p} on each parameter (G4, as bash);
        $LINENO (G8, as bash; not in eval or trap text); UID an ordinary
        variable where the string sets it (G15, as dash); ~ without HOME from
        the password file (G16, as bash), and HOME may be set and unset; break
        and continue in a subshell inside a loop end it (as dash).  jobs takes
        %N, %%, %+, %-.
      - K3 (the user's choice B, 2026-11-03): background jobs go on between
        run() calls too: while they remain, Proc::ShellSpawn keeps a deferred
        SIGCHLD handler installed, which calls the new Proc::Spawner::poll().
        Cost (documented in doc/pspawn-design.md, section 6, and the POD): the
        program's own sleep and select return early when a child ends.  Test
        t/between.t.
      - Q2 decided (the user's choice B): printf -v stays with the program.
      - doc/checklists/corpus-gaps.html and its sources (doc/large-subset/pages/)
        are no longer shipped: all its questions are decided (they are in 0.04).
      - X25 fixed (a bug since 0.01): break or continue in a pipeline element
        inside a loop died with a Perl error.
      - Hand-kept verdict "no difference" for the new parser test LS:299
        (kill() { :; }; kill -SIGHUP 1): the detector flags kill -SIGHUP, but
        both shells run the function (probe fnkill).
      - doc/pspawn-spawner.md: section 8 (Windows) is in the distribution.
        (Corrected post4-0.04: it was not "in 0.01 but lost by 0.04"; no CPAN
        release had it, only the Project's later copy of 0.01.)
      - Detector: atpat, lineno, nohome, uidassign, SC3058 are class X (rule E);
        uidread is the old bashvar for UID.  Battery
        t/batteries/large-subset-post04.txt.
      - Figures: 91 samples rejected within the lower yardstick (0.04: 95);
        digest 1b9eac74d672e79d89764f71b1a6125e4a52f4cdc42943a6db39ec3d0b6d95aa.

0.04  2026-11-03
      - The documents of only historical interest are no longer shipped; they
        are in Proc-Spawner 0.03 on CPAN: doc/large-subset/large-subset-
        {ceiling,investigation,marks,upstream-reports}.md, doc/pspawn-run-
        shortcomings.md, doc/pspawn-upstream-reports.md, doc/parsers/shell-
        subset-corpus-stats.md.  References to them say so.
      - The decision pages that are no longer relevant are no longer shipped
        either (they are in 0.03): in doc/checklists/ child-exit, run-graph-api,
        spawn-graph, large-subset and dialect-choices.html, and in
        doc/large-subset/pages/ their sources and build.pl.  Only
        corpus-gaps.html (question Q2 is open) and its sources stay.
        Known issue X1 now names the alternatives of K3 itself.
      - The figures of post4-0.03 were checked again on 0.04: the same per-sample
        results; they are labelled 0.04.  Digest of the code they depend on:
        ecde8b9b9610680560a69e56313f01687f29e51688bcd029c6cef837fa03eeac.
      The documents label the unreleased states between 0.03 and 0.04 post-0.03
      to post4-0.03; their entries follow, newest first.

    post4-0.03:
      - Rule E (the user, 2026-11-03), replacing A3: where dash and bash
        differ, the subset takes the plainly logical behaviour, and rejects the
        form only where there is none.  doc/large-subset/large-subset-logical.md
        lists every difference with its category (info, error, posix, same,
        harm, platform, meaning, effect, none) and checks the earlier decisions
        against the rule.  Done so far: jobs, jobs -p, jobs -l list the jobs also
        in a pipeline and in $( ) (as bash; this replaces the rejection of
        post3-0.03); return outside a function is an error (as bash); cd with
        extra operands or without HOME is an error (as bash); descriptors above 9
        (option fdmulti now on by default); [^...] negates; patterns never match
        . or ..; settings with a redirection are redirected before their values
        are expanded (as dash).  getopts leaves OPTARG unset when it finds no option, or
        an option without an argument (POSIX; as bash; dash keeps or empties it).  Battery t/batteries/large-subset-e.txt (the
        differences from dash are listed in t/dash-known.txt as rule E).
      - Forms both shells run alike, now accepted: reading $PPID (assigning it
        stays rejected), export "$name", symbolic umask modes, redirections on
        cd, and the top-level jobs -p.
      - X19 fixed: a trap's action now runs before the next command of the list
        (it could run after it, about 1 run in 30).  New known issue X24.
      - The detector: the choices of rule E are class X with basis E:CATEGORY;
        ShellCheck's SC3023 and SC3026 likewise; local and return outside a
        function are separate features; local=... is no longer taken for local.
      - Figures: 95 samples rejected within the lower yardstick (post3-0.03:
        106); digest 84245e1d9a2ad88da10b320f8d2e94ddeb0b12bd888925555b920d985026e10a.

    post3-0.03:
      - Proc::ShellSubset::Large rejects jobs in a pipeline, a command
        substitution or a compound command: there dash lists nothing and bash
        the jobs (probes jobspipe, jobsppipe, jobspsubst).  Tests LS:288-292.
      - The detector has two more blatant features: jobs in a pipeline or $( )
        (jobsctx), and cd with extra operands (cdargs: dash ignores them, bash
        fails).  Its word pattern now keeps $( ), backquotes, ${ } and
        backslash-escapes in one word.  11 new probes (jobs, cd, the P6 forms) in
        doc/large-subset/dialect-cases/large-subset-dialect-probes2.txt.
      - Figures: 106 samples rejected within the lower yardstick (post-0.03:
        117); digest 64810b85d9f4cc777ef7d41f3cc3f9f4262c1c56cf877ba00489dbce07b117e8.

    post2-0.03:
      - pwd runs as the program /bin/pwd -L, with PWD set to the shell's own
        directory, and its operands dropped (known issue X23; the user's choice C,
        2026-11-03): after cd through a symbolic link it prints the directory as
        reached, as both shells (before: the physical path).  Two more lines in
        t/batteries/large-subset-p6.txt (21 of 21 agree with dash).  The parsers
        did not change, so the figures of post-0.03 hold.

    post-0.03:
      - Proc::ShellSubset::Large accepts more forms both shells run alike (item P6
        of "Shell Corpus Gaps"; probes in doc/large-subset/dialect-cases):
        read -p PROMPT (also -rp; the executor writes the prompt to fd 2 only
        when fd 0 is a terminal, as both shells); cd -P and cd -L (the last one
        counts; the tree gets cd_p => 1, and the executor resolves symbolic
        links); a pattern in a redirection target is literal (echo a >*.log
        writes the file '*.log', as dash and bash in POSIX mode); set -o
        ignoreeof, notify, nolog, set -b and set +m (nothing to do); trap with
        signal names in any case and signal numbers, also FPE, and KILL and STOP
        (cannot be caught: nothing to do, status 0).  Tests LS:273-287 (LS:114
        now accepted), and t/batteries/large-subset-p6.txt (19 of 19 agree with
        dash).
      - pwd with an option both shells refuse (pwd -x, pwd --help) is rejected
        (rule Q1; test LS:111 changed).  Known issue X23: a bare pwd after cd
        through a symbolic link differs from both shells (the program /bin/pwd,
        decision B2 of the base parser's checklist) - a question to the user.
      - The detector (tools/large-subset-features.pl): a read -p prompt with
        quotes no longer hides the options after it (read -p "`echo -e x`" m
        was taken for read -e), read -p PROMPT without a name is the bash-only
        REPLY form, and set -o pipefail and the like are seen after other
        options (set -euo pipefail).
      - Figures (doc/large-subset/large-subset-state.md): the samples rejected
        within the lower yardstick went from 163 (0.03) to 117.  They depend on
        the parsers, the measurement scripts and the verdicts, whose sha256
        (the command: doc/large-subset/large-subset-state.md, section 3) is
        4921dfbd1b838bc48dabe234a5111ad69d8fbb7d43c8278c72dcf74a5ad9b7bf.

0.03  2026-11-03
      - Proc::ShellSubset::Large accepts forms dash and bash run alike (items
        P1-P4 of "Shell Corpus Gaps"): '{' and '}' inside words where no brace
        expansion can start; $( ), `...`, ${#NAME} and $((...)) inside $((...));
        setting PS1, PS4, HOSTNAME, PIPESTATUS and BASH; export $v[=value];
        exit N above 255; umask -S; command -p PROGRAM.  '{' or '}' as a
        command name, and '{NAME}' right before a redirection (bash: a named
        descriptor), stay rejected (tests LS:212-245).
      - Proc::ShellSpawn, Proc::ShellSpawn::Large: run those forms ($( ) inside
        $(( )) expanded first; exit N modulo 256; umask -S; command -p with
        /usr/bin:/bin; export with a name from an expansion, checked at run time).
      - The borrowed test pool: the scripts that made
        t/parser/shell-subset-corpus-tests.pl are now in doc/parsers/tools/
        (extract, run, group, gentests, prov, verify and the reports), with
        the grouping of 2026-10-29 (groups.tsv); doc/test-pools.md says how to
        rebuild every pool and measurement from the pinned sources (checked:
        all 612 snippet IDs restore their texts exactly).
      - Measuring against dash and bash by the shells alone:
        doc/large-subset/large-subset-state.md (the rules, which beats which,
        the figures, the work list); large-subset-dialect-features.md (every
        probed feature, blatant or subtle, with who decided it);
        tools/large-subset-features.pl (the detector, and the per-test record
        t/parser/large-subset-shells.tsv), tools/large-subset-verdicts.txt
        (verdicts kept by hand, postponed ones as free text), and a new
        tools/large-subset-yardsticks.pl (the old one sorted our parser's
        own messages).
      - doc/pspawn-known-issues.md: X21 (the programs echo, [, printf, kill on
        Windows), X22 (bash 5.2.21 goes on after some special-builtin errors,
        against its manual).
      - Items the user agreed to on "Shell Corpus Gaps" (2026-11-03):
        B1 alias and unalias (the parser expands aliases in the lines after a
        top-level definition, as both shells do; the executor keeps the table
        for eval and . text); B2 history, fc, bind, help, compopt, logout and
        set -o history do nothing by default (parser and run() option
        interactive => reject|nop|dash), and the executor looks command names
        up in a list of tables, @Proc::ShellSpawn::CMDS, which other modules can
        extend; B3 jobs, bg, fg, hash, type, times and reading ulimit, with
        dash's texts and statuses, in the new module Proc::ShellSpawn::Builtins;
        P5 and R1 [ and test are the program, and '<' or '>' between two
        operands need the parser option test_lt_gt; Y2 a backslash at the very
        end is a literal \; Y3 tldr's <Enter> in one-line here-documents, and
        lines with keystrokes left out of the measurement; Q1 kill with a SIG
        signal name is rejected (both shells refuse it).
      - Every measured figure in doc/ names the version it was measured on
        (pre-0.01, 0.01, 0.02, early-post-0.02, 0.03; the table is in
        doc/README.md).  The figures of 0.03 were measured again on the release:
        the corpus yardsticks, the work list and the totals over the tests came
        out the same.  Corrected: spec section 8's ShellCheck figures (now as in
        the ceiling document), test-pools.md's size of the per-test record
        (1,066 lines), and the count of samples accepted only thanks to class X
        (258, not 259); test-pools.md's regrouping count is of 0.03 (577 groups,
        checked).
      - doc/checklists/corpus-gaps.html is the page "Shell Corpus Gaps" as
        published (its third version, with the figures of 0.03), and
        doc/large-subset/pages/gaps-items.txt.lede its lede (the lede had been
        edited only in the published page).  t/dash-known.txt no longer lists the
        line of largebattery.txt with an alias as a known difference: since B1 it
        agrees with dash, so a change there now fails t/dash.t.
      - lib/Proc/Spawner/Docs.pod: a map of the Markdown documents in doc/;
        doc/large-subset/large-subset-state.md.html: the same text as the .md,
        with a Markdeep footer (an experiment in how MetaCPAN shows them).
      - Proc::ShellSubset::Large no longer claims the package name
        Proc::ShellSubset for the PAUSE indexer (MetaCPAN showed it as the
        home of Proc::ShellSubset).

0.02  2026-11-02
      - Documentation: POD for Proc::ShellSubset, Proc::ShellSubset::Small and
        Proc::ShellSubset::Large; doc/large-subset/ (the large parser's
        specification, how its corpus coverage is measured, with the scripts in
        doc/large-subset/tools/, and its history); doc/parsers/ (the base
        parser's grammar and the provenance of its corpus tests); three more
        decision pages in doc/checklists/.
      - t/parsers.t also checks that Proc::ShellSubset::Small parses every test
        string as Proc::ShellSubset does, and, with dash and bash in PATH, that
        both shells accept what only the large grammar accepts; the checks that
        need dash and bash are skipped without them (they used to fail).
      - The message for a construct outside the subset found at run time
        (status 248) now names Proc::ShellSubset::Large, not ShellSubsetLarge.
      - README: the aim is matching dash/bash (what both do alike), not dash
        alone; also portable shell scripts; every started program is a child of
        the Perl process.  Proc::ShellSpawn's header says the same.
      - Every module has $VERSION.
      - t/dash.t: each run has its own tmp/ for the battery strings that wrote fixed
        files in /tmp, and ignores the length of process IDs turned into N.
        Test runs at the same time no longer fail each other (probably the
        unexplained failure X20).  The kill -INT $$ strings are marked FLAKY:
        X19 also shows with a subshell.

0.01  unreleased (2026-11-02)
      - First version: Proc::Spawner (posix_spawn with every glibc 2.39 setting, the
        one event loop), Proc::Spawner::Graph (the executable spawn graph),
        Proc::ShellSpawn (runs shell one-liners without fork), and the parsers
        Proc::ShellSubset, Proc::ShellSubset::Small, Proc::ShellSubset::Large.
      - Builds with older glibc (seen failing on 2.36): Makefile.PL probes for each
        posix_spawn extension, and an option the libc lacks follows UNSUPPORTED.
      - No "Hexadecimal number > 0xffffffff" warning from t/spawner.t or $((0x...)).
      - Tests: t/graph.t, t/parsers.t, t/dash.t besides t/spawner.t.
      - Former names: PosixSpawn (now inside Proc::Spawner), ShellSpawn,
        ShellSubset, ShellSubsetSmall, ShellSubsetLarge.
