Synopsis:
This page covers CSharp_TextFinder_Output, the sink. It receives fully formed
strings, writes each as one line, and adds nothing but the terminator.
-
Two write methods differing only in what they add, and the placement is the access
control -
WriteText is on the class rather than the interface, so
traversal cannot emit unterminated text.
-
The class is
sealed, since a sink whose Output a subclass
could override would put the emission order in the hands of a type this specification
does not describe.
C# is the language the terminator rule was written for, and this component is where the
prediction gets tested.
Console.WriteLine terminates with Environment.NewLine, so the framework's most obvious way to write a line is the one the specification forbids.
- The rule was promoted to the parent specification on the C# case before any C# code existed. It held.
Console.Out is not used at all - the sink wraps the standard output stream directly, so nothing a library loaded later reconfigures can reach this program's output.
One sink, one buffer, and failure absorbed rather than reported.
- A second sink throws, because two buffers over one stream reorder the output silently and only under load.
- The rule costs one static field here, where Rust needs two constructs and C++ needs none at all.
- A failed write flushes, writes one notice, and discards everything after - and no caller ever learns of it.
1. The Sink
CSharp_TextFinder_Output receives fully formed strings from
CSharp_TextFinder_Dirnav, writes each as one line to stdout, and absorbs any
write failure so that neither the traversal nor the binary has to reason about it.
public sealed class StdoutSink : IOutput, IDisposable
{
public StdoutSink();
public void Output(string text);
public void WriteText(string text);
public void Flush();
public void Dispose();
}
The class is sealed. Nothing in this project derives from it, and a sink whose
Output a subclass could override would put the emission order §3.4 fixes
in the hands of a type this specification does not describe. C++ reaches the same place from
the other direction, its sink deriving from a base class it may not itself be derived from
in practice; Rust has no inheritance to close off.
Two methods write, and they differ only in what they add.
Output, the interface method, writes the string it is given followed by
the single LF Spec_TextFinder.md §3.4 fixes. It is the method
CSharp_TextFinder_Dirnav reaches, and the interface is the only thing that
library knows about this one.
WriteText writes the string verbatim, adding nothing. It exists for the
help text of §5.1 and the option listing of §5.3, which
CSharp_TextFinder_Cmdline returns already terminated.
WriteText is declared on the class and not on IOutput, and that
placement is the access control: CSharp_TextFinder_Dirnav holds a
TOutput constrained to the interface and can reach only what the interface
declares, so it cannot emit unterminated text. Both route through one private
Emit(text, terminate), so the failed-state check and the write have one
definition.
The type takes no configuration. CSharp_TextFinder_Dirnav formats every line in
full before emitting it - a block's path line, a block's indented detail lines, and every
announcement alike - so there is nothing left here to parameterize. The library does not
know a block line from an announcement, does not indent a detail line, and does not apply
/h, /n, or /L.
Once constructed, neither method throws and neither reports failure to a caller.
2. The Rule Written for This Language
Spec_TextFinder.md §3.4 obliges an implementation to prevent its runtime translating
the LF terminator to CRLF. C# is the language that obligation was written for.
Console.WriteLine terminates with Environment.NewLine, which is
CRLF on Windows, so the framework's most obvious way to write a line is the one the
specification forbids.
The rule did not start in the parent. It lived in the
C++ Output specification
for several turns, binding C++ alone, and the cross-document review promoted it to
Spec_TextFinder.md §3.4 with the C# case as its argument - before any C# code existed.
This component is where that prediction gets tested, and it held.
The sink owns a StreamWriter over Console.OpenStandardOutput()
with three properties fixed by the specification rather than left to the code:
NewLine is "\n", so
WriteLine emits one LF on every platform.
AutoFlush is false, per Section 5.
- The encoding is a
UTF8Encoding constructed
encoderShouldEmitUTF8Identifier: false, so no byte-order mark
precedes the first line. Encoding.UTF8 is rejected by name: that
property's encoding emits a BOM when a StreamWriter opens a stream with it,
which would put three bytes on stdout that §3.4 does not fix and that a fixture
comparing bytes would see.
Console.Out is not used at all, and neither is Console.Write. The
writer this library constructs wraps the standard output stream directly, so nothing the
framework configures on Console.Out - its encoding, its newline, its autoflush
- reaches this program's output. That is a stronger position than configuring
Console.Out would give, since a library loaded later could reconfigure it.
One requirement, three implementations, three mechanisms: C++ calls
_setmode(1, _O_BINARY) and throws if it fails; Rust does nothing at all, its
standard output performing no translation on any platform; C# replaces the writer. The
integration suite
checks the bytes rather than the characters, and finds no CR and no BOM.
Output adds the terminator and nothing else - no prefix, no separator, no
trailing content - because the string arrives fully formatted.
3. One Sink, One Buffer
The constructor throws InvalidOperationException when a
StdoutSink already exists, so the process holds one sink and one only. It also
lets an IOException from Console.OpenStandardOutput propagate,
which is the failure of a process started with no usable standard output handle.
CSharp_TextFinder_Entry catches both and exits 2, the code §3.4 fixes for
a failure that is not about the command line.
The reason for the rule is the buffer of Section 5. Two sinks would wrap the same standard
output stream with two independent buffers, and their contents would reach the stream in the
order the buffers happened to fill rather than the order the lines were written - which
would break the emission order §3.4 fixes, silently and only under load. A rule against
a second sink is cheaper to enforce than a defect that appears only on long searches.
The rule is held by a private static bool, cleared by Dispose, so
a sink disposed before another is created releases the right to make one. A static mutable
field is a smell in general and the specification argues that it is right here: the thing
being guarded is a process-wide resource, one process holds one standard output handle, and
the field is read and written on one thread before any other could exist. No lock is taken.
Rust reaches the same conclusion through a thread_local! Cell,
which it needs because a plain static there must be Sync. C#
places no such bound on a static field, so the same rule costs one field here and two
constructs there. C++ has no equivalent rule at all, its sink being a local of
main that nothing else constructs.
CSharp_TextFinder_Entry builds exactly one sink, so its own code cannot provoke
the exception. It handles the case because a constructor with a failure mode that callers
ignore is a constructor whose failure mode will eventually be reached, and because that
exception is what gives exit code 2 a path in this implementation at all.
4. Error Handling
A write that fails - a closed pipe, a full disk - sets an internal failed state. On the
first such failure the library flushes stdout and then writes the single line
output failed to stderr. Thereafter it discards every string it is given and
writes nothing more, to stdout or stderr.
Three properties of that sequence are stated in the specification rather than left to the
code.
- The flush comes first, so every line already buffered reaches the
stream ahead of the notice explaining why the lines stop. It is best-effort: whatever
broke the write may break it too, and that inner flush is itself wrapped.
- The failed state is permanent. One notice, not one per discarded
line, which is what keeps a broken pipe from turning a long search into a long stderr
transcript.
- The failure reaches no caller. The library never throws after
construction, never returns a status, and never lets the failure reach
CSharp_TextFinder_Dirnav, which goes on traversing.
Every write is wrapped against IOException and
ObjectDisposedException, the two the framework's stream writes document, and
the specification refuses a broader catch in as many words: an
OutOfMemoryException or a NullReferenceException from this library
is a defect in it rather than a stream failure, and swallowing one would hide that defect
behind a notice about output. The two types are named in one predicate,
IsWriteFailure, used from every catch filter in the file.
A method returning void where the operation returns a status is usually worth
questioning, and here it is the interface the structure document asked for. The
IOutput method returns nothing, so absorbing the failure is the contract; the
alternative is an interface method returning a status and a traversal that must decide what
to do about a sink it was given rather than chose.
The flush performed on Dispose obeys the same rule, and the specification names
that case separately for a reason: the final flush is the write most likely to be the first
one that fails, being the only one that must reach the stream and, on a short search, the
only one that reaches it at all. Nothing follows it, so nothing is left to discard.
A failed write does not affect the exit code, which the Entry specification reserves for
command-line and startup failures. A run whose output went nowhere still exits 0: the exit
code answers whether TextFinder could do what it was asked, not whether the reader received
it.
The notice is written with Console.Error.Write and an explicit
\n, so stderr carries LF here as it does everywhere else in this
implementation.
5. Buffering and Disposal
AutoFlush is false, so the StreamWriter defers a
write until its buffer fills. With AutoFlush set the writer would flush on
every call, and a search emitting 79 lines - the count invocation 2 of the
demonstration
reports - would pay 79 system calls for nothing.
No flush is performed per line. The stream is flushed when the sink is disposed, and
Flush performs the same write on demand, for the one case that needs the buffer
drained before the process ends.
Disposal is the whole of the mechanism, and C# leaves no alternative. The language has no
destructor that runs at scope exit, so the C++ implementation's reliance on one and the Rust
implementation's on Drop have no counterpart here. A sink that flushed from a
finalizer would flush at a time the runtime chooses, and the runtime is free to run no
finalizer at all before the process exits - which on a short search would mean losing the
whole output.
That moves the guarantee out of this type and into its caller, and both specifications say
so on their own side. This one states the requirement and records that the type cannot
enforce it alone; Spec_CSharp_TextFinder_Entry.md §4 discharges it with a
using statement covering the rest of Main. The
Entry page covers why the
construction sits outside that statement.
Dispose is written to be safe to call twice, since a using
statement on a sink already disposed by other means would otherwise fault. A second call
flushes nothing and clears nothing. The unit suite asserts it, because the property is one a
later edit could remove without any other test noticing.
One rule keeps the deferral from reordering the output:
stdout is flushed before any write to stderr.
This library applies the rule to its own output failed notice, and
CSharp_TextFinder_Entry applies it to the diagnostic that follows the option
listing on an invalid /r. Invocation 10 of the demonstration is that case: nine
listing lines on stdout, then two diagnostic lines on stderr, in that order.
Because this object owns the only writer over standard output in the process, the binary's
help text and option listing pass through WriteText rather than through a
writer of their own. They therefore share this buffer and reach the stream in the order
written, and the ordering rule above is the only coordination needed.
6. Source
StdoutSink.cs in full. At 137 lines it is the second-smallest source in the
project, and the ratio to its 115-line specification is the point of the component: a sink
that does one thing can be specified exhaustively.
src/StdoutSink.cs
// StdoutSink.cs - the process's one stdout writer, per Spec_CSharp_TextFinder_Output.md
using System.Text;
using CSharp_TextFinder_Dirnav;
namespace CSharp_TextFinder_Output;
// §4: sealed, so no subclass can override the emission order Spec_TextFinder.md §3.4 fixes.
public sealed class StdoutSink : IOutput, IDisposable
{
// §4: the one-sink rule. A static field needs no lock here: one process holds one
// standard output handle, and this is read and written on one thread before any
// other could exist. Rust needs a thread_local Cell for the same rule because a
// plain static there must be Sync; C# places no such bound on a static field.
private static bool _taken;
private readonly StreamWriter _writer;
private bool _failed;
private bool _disposed;
public StdoutSink()
{
if (_taken)
{
throw new InvalidOperationException("a StdoutSink already exists");
}
// §5: Console.WriteLine terminates with Environment.NewLine, CRLF on Windows, so
// this writer is built over the standard output stream with NewLine fixed to LF.
// Encoding.UTF8 would emit a byte-order mark; this encoding does not.
Stream standardOutput = Console.OpenStandardOutput();
_writer = new StreamWriter(standardOutput, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = false,
NewLine = "\n",
};
_taken = true;
}
// §4: the interface method. The string, then the single LF §3.4 fixes.
public void Output(string text) => Emit(text, terminate: true);
// §4: declared on the class and not on IOutput, so Dirnav cannot emit unterminated text.
public void WriteText(string text) => Emit(text, terminate: false);
// §7: for the one case that needs the buffer drained before the process ends.
public void Flush()
{
if (_failed || _disposed) return;
try
{
_writer.Flush();
}
catch (Exception e) when (IsWriteFailure(e))
{
Fail();
}
}
// §7: disposal is the whole of the flush mechanism. C# has no scope-exit destructor,
// and a finalizer may never run, so the caller's using statement carries the guarantee.
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (!_failed)
{
try
{
_writer.Flush();
}
catch (Exception e) when (IsWriteFailure(e))
{
// §6: the final flush is the write most likely to be the first that fails.
_failed = true;
Console.Error.Write("output failed\n");
}
}
try
{
_writer.Dispose();
}
catch (Exception e) when (IsWriteFailure(e))
{
// Dispose flushes again; the notice above already reported the failure.
}
_taken = false;
}
private void Emit(string text, bool terminate)
{
if (_failed || _disposed) return;
try
{
if (terminate)
{
_writer.WriteLine(text);
}
else
{
_writer.Write(text);
}
}
catch (Exception e) when (IsWriteFailure(e))
{
Fail();
}
}
// §6: flush first, then one notice, then discard everything after. The flush is
// best-effort, since whatever broke the write may break it too.
private void Fail()
{
_failed = true;
try
{
_writer.Flush();
}
catch (Exception e) when (IsWriteFailure(e))
{
}
Console.Error.Write("output failed\n");
}
// §6: the two the framework's stream writes document. A broader catch would hide a
// defect in this library behind a notice about output.
private static bool IsWriteFailure(Exception e) =>
e is IOException or ObjectDisposedException;
}
Dispose repeats the failure handling rather than calling Fail,
because Fail flushes first and the flush is what has just been attempted. The
duplication is four lines and the alternative is a flush of a buffer that was reported
unwritable one statement earlier. The _writer.Dispose() that follows is wrapped
for the same reason: it flushes again, and by then the notice has already been written.
7. Prompt Records
This page carries none. Page_Structure.md §8 assigns it
Prompts_Spec_CSharp_TextFinder_Output.md and its Fix companion,
and neither was written: this thread produced one record covering every turn, and it sits on
the Process page, whose
Section 3.2 traces the terminator rule from the C++ specification through the parent to this
component.