A command’s value is its output: F carries a grade
Amended 2026-10-19 by await-is-the-block-run-here:
Handlecarries its block’s grade,Handle^ε α(printedHandle (Returns α),Handle CommandorHandle (ν α)), andawait : ∀ε α. Handle^ε α → F^ε α. Sospawnandwatchno longer absorb their body’s grade:awaitofspawn { echo hi }is aCommand, andlet x = await $hbinds"hi".serviceanddeferare deleted, and with them their rows in the absorbing list;each,fold,fold-linesandauditstill absorb.
F carries a grade. F^p A produces a value A (printed Returns A);
F^w Unit produces output and nothing else (printed Command). Where a value is
demanded of a command, the command is captured and its output decoded as a
String; where a command is demanded, a ()-producer runs as one. The fact
“this computation’s result is its output” lives in the type, so it passes through
abstraction, application and variables, and β/η preserve meaning.
What was decided
- The grade is a producer kind, not an effect.
{ echo pre; return 5 } : F^p Int: the grade says how the result is produced, not whether anything is written. That keeps the bind rule total and the arithmetic small. - Types.
CompTy::Return(Grade, Ty),Grade::{Value, Output, Var},CompTy::pureisF^p,CompTy::command()isF^w Unit. A grade variable prints fromν ξ ο π;explain retryis∀α ν. Integer → {ν α} → ν α. - Invariant
F^w A ⇒ A = Unit. Rule-enforced, not representational: every rule that introduceswdoes so atUnit, every rule that copies a grade copies the value type, every builtin scheme pairs a grade variable with one value variable. Grades are atomic (a grade variable binds onlyporw), so there is no occurs check, no kind, and a grade is never weak. - Introductions. Externals,
^nameand path heads areCommand; so areecho,to-json,to-string,to-line,to-lines,to-jsonl,to-csv,to-bytes,ints-to-bytes,help,explain,clearandreset.warnisReturns Unit(stderr is not the byte channel).spawn/watch/service,each,fold,fold-lines,auditanddeferare polymorphic in their body’s grade and absorb it;fail,exitanddivergesinhabit every grade;map,filterandsort-list-bydemand a value. - The bind rule.
M : F^p AbindsA;M : F^w UnitbindsString, elaboratedcap M to d. decode d;M : F^ε̂ Asetsε̂ := p. Aletis a demand for a value, and this is the only defaulting in the system; there is no settle phase for grades. Solet x = Mislet x = M | from-linefor anyMthat is a command. - One run-time coercion, one upcast.
cap : F^w Unit ⇝ F^p String, inserted only in checking mode, at a demand already resolved toF^p: a bind, an argument (a callee parameter ending inF^p βat the same arity against an argument ending inF^w Unit; a block in hand is η-wrapped), a join that settles onp. The upcastF^p Unit ⇝ F^w Unitis identity at run time and is admitted at joins, handler arms and arguments; fromF^p Unitthere is no path toF^p String, so the two coercions are coherent. Elsewhere a later demand is an ordinaryCompTyMismatchwhose hint names the fix. - Joins take two passes. Infer every arm against a fresh result, resolve,
then: any
F^p Aarm withAnotUnit(or unresolved) makes the targetF^p, capturing eachF^w Unitarm; else anyF^w Unitarm makes itF^w Unit, upcastingF^p Unitarms; else unify.if $c { hostname } else { echo b }is oneCommand;if $c { echo $x } else { warn skip }is aCommandin any position;try { fetch } { |e| return 'none' }bindsfetch’s output. - Pipes and redirects. A stage before the last is accepted at
Unitin any grade. A decoder mid-pipeline is refused by its own mark (T0078DecoderMidPipeline), not by grade. A stdout redirect discharges the grade:M > f : F^p A, solet x = to-json 1 > fbinds(); stdin and stderr redirects and2>&1preserve it. - Scopes and handlers.
within,grantandguardpass the body’s grade through;auditabsorbs it and returnsF^p Report(α). An arm for an external stands in at[String] → Command; a()-returning stub is admitted and captures""; the installed scheme carries the head’s producer kind. - The tail
Runis never coerced. - Deleted.
capture_sites,capture.rs,head_writes,stage_returns,tail_writer,arm_writer,Output/with_output/output:,Inferencer::capturedand thewriterfield of the join reasons.HeadClassmoves toinfer.rs.
The twelve decisions
- Annotation on
F, not aCmdconstructor:Valstays trivial and grade polymorphism is ordinary let-polymorphism. - Tail grading, not effect union: union grading needs a lub at every
;, hence constraints, and would reopen “value or output?” forF^w Int. - One run-time coercion
F^w Unit ⇝ F^p String, at demands resolved top, in checking mode; one zero-cost upcastF^p Unit ⇝ F^w Unit, at joins, handler arms and arguments; neverCommand ⇝ Returns (), which is incoherent with the first. - Bind defaults an unresolved grade to
p; callers meet the demand at the argument. No pending constraints. - Pipe stages accept any grade at
Unit; the decoder refusal is by the decoder’s mark. - Redirects discharge.
map/filter/sort-list-bydemand a value;each/fold/fold-lines/spawn/watch/service/auditabsorb.parfollowsmapby inference.- Handler arms take the head’s producer kind.
- Whole-RHS capture:
let x = M ≡ let x = M | from-line. - The tail
Runis never coerced. - Printing:
Returns A,Command,ν A; noF^in user-facing text. - No compatibility: the wire and baked-prelude formats change freely.
Why
The old design put the fact in a syntactic walk of the let’s right-hand side
(capture-is-decided-by-syntax),
so let f = { echo hi }; let x = f bound () and printed, map { |f| echo $f } [1, 2] gave [(), ()], let x = !{ echo pre; echo host } bound only "host",
and a redirected writer silently captured "". Three walkers (capture_sites,
stage_returns, tail_writer) approximated one missing fact. A grade in the
type turns the walkers into ordinary unification and makes the user-visible rule
a single sentence.
What it costs
- A per-call wrapper. A coerced block in hand is η-wrapped, allocating a closure per coerced argument, only where a value was demanded of a command.
- Buffer limits reach more. The 16 MiB capture cap and strict UTF-8 decode
now apply to whole right-hand sides and to each
mapelement. - Stored sites. A
Siteshape now carries a grade; a seed from an older binary does not decode (accepted, decision 12).
The known limitation
Joins are decided in program order with no pending constraints, so the verdict is
principal on the stated rule but not order-independent:
{ |t| if c { echo hi } else { !$t }; let w = !$t; $w } types t : {Command},
while the same block with the let first types t : {Returns String}.
Supersedes
capture-is-decided-by-syntax:
who places the capture changes, from a syntactic walk to the type. Its
statements that pipes stay positional, that an arm stands in for what it
replaces, and that { … } is a thunk in every position stand.
The plan is dev/docs/plans/260930_graded-f.md.