A fresh head defines its own modes; preservation binds only a known head
A handler or alias reinterprets a head, so where a head already has modes the
arm must agree with them — but a brand-new alias or handler name is the head:
there is nothing prior to preserve. head_pipe_spec’s unknown case yields a
fully fresh F[μ, ν] and the arm defines the head’s modes; preservation is
retained exactly where a known head’s modes exist to be reinterpreted, and the
byte-channel discipline that does the real work moves to the connection point.
- An unknown head is no longer pinned to the external byte default.
head_pipe_spec’sNonearm (core/src/typecheck/infer.rs) mints a fresh input and output mode rather than the externalF[μ, Bytes].pin_arm_to_headunifies the arm’s spec against that fresh spec — a no-op constraint — sohandler_comp_schemelets the arm’s own modes stand. An alias whose body iterates values (… | from-lines | { |x| … }) installs as a value-output command. - A known head still constrains the arm. When
lookup_handlerfinds a scheme whoseCompTyresolves to aReturn, itsPipeSpecis the head’s, and pinning an incompatible arm against it is a positionedModeMismatch. Reinterpreting an existing handler with a value output under a byte-output head is unchanged: still rejected, still at the definition. - The byte-channel discipline is the real invariant, and it is enforced at the
connection.
O_left = I_right(docs/SPEC.md§4.2.1) holds where pipeline channels meet, not at the alias definition. A value-output head piped into a byte consumer is still a mismatch — now reported at the use site where the edges connect rather than eagerly at the head’s definition. The rule moved from definition-time pinning to connection-point checking; soundness is unchanged. - A
?fallback chain carries its arms’ modes.infer_chainunions the arms’ input and output modes viaunion_mode— the same wideningmerge_branchesgivesif, since only one arm runs — while the chain’s value type stays a fresh variable (the arms’ value union is unspellable). A chain of byte-output arms (tmux a ? tmux b) reads as byte-output and so installs as an alias over a byte head, where the prior value-edge default discarded the arms’ modes.
Why now: unconditional-mode-pass
made plugin files and rc aliases fatally checked by the single static checker.
Under eager definition-time pinning, ordinary value-output aliases and ?-chain
aliases could not install. Letting an alias or handler be a command that defines
its own modes — keeping preservation only where a head exists to be
reinterpreted — is the correct resolution, not a weakening: the byte discipline
still binds, at the edge.
This supersedes handler-alias-mode-preservation, which pinned every unknown head to the external byte default and so read a value-output arm as a static mismatch at the definition.
See also
unconditional-mode-pass,
modes-equality-constrained-shared,
typecheck,
effects-handlers;
the byte-edge judgment is docs/SPEC.md §4.2.1.