sysl

Module

sysl.io

bytes_reader bytes_reader_at_most bytes_writer console_lines fd_reader find_byte line_text lines lines_ending stdin try_line_text BytesReader BytesWriter FdReader LineEnding Lines Reader Fallible for BytesReader Fallible for BytesWriter Fallible for FdReader Iterate for Lines Reader…

Import import sysl.io

Index

bytes_reader bytes_reader_at_most bytes_writer console_lines fd_reader find_byte line_text lines lines_ending stdin try_line_text BytesReader BytesWriter FdReader LineEnding Lines Reader Fallible for BytesReader Fallible for BytesWriter Fallible for FdReader Iterate for Lines Reader for BytesReader Reader for FdReader Writer for BytesWriter

Functions

bytes_reader

bytes_reader(b: []const u8) -> BytesReader

A reader that hands back as much of what is left as the caller offered room for.

bytes_reader_at_most

bytes_reader_at_most(b: []const u8, most: usize) -> BytesReader

A reader that hands back at most most bytes per read, however much room it was offered.

This is how a consumer’s refilling is tested, and there is otherwise no way to do it. Anything reading a stream has cases that only arise when a unit of input straddles two reads – the two bytes of a CRLF, an escape sequence, the continuation bytes of one character – and a reader that always empties itself in one go never produces them. Those cases are exactly the ones a hand-rolled reader gets wrong, so being unable to reach them is being unable to test the part that breaks.

It is also what a slow source honestly looks like: a serial port hands over what has arrived, not what was asked for.

bytes_writer

bytes_writer() -> BytesWriter

console_lines

console_lines(r: *Reader) -> Lines

A cursor over a terminal: \r, \n and \r\n each end a line.

It is a name of its own rather than a flag on lines because the reason to reach for it is where the bytes come from rather than a setting to be looked up, and “I am reading from a console” is the thing a caller actually knows.

fd_reader

fd_reader(fd: int) -> FdReader

find_byte

find_byte(b: []const u8, c: u8) -> Option[usize]

A line is found with memchr rather than a byte loop because libc’s reads a word at a time and sysl’s could not. This is the whole of what pointer difference is for: memchr answers where with an address, and an index is that address minus the first.

line_text

line_text(b: []const u8) -> string

Bytes to text at the boundary they arrive at, which is where 04 puts the check. A trailing \r goes with the \n, so input written on one system reads the same on the other – bufio.Scanner‘s choice rather than C getline‘s.

Ill-formed input stops the program the way an ill-formed argument does, and that is the right severity for a program reading a file it expects to be text: there is nothing sensible for it to do with the rest, and carrying on would put the mistake a long way from its cause.

Where that is the wrong severity, try_line_text is the same conversion without it. Input off a wire, out of a serial port, or from a file somebody else wrote carries whatever was sent, and a program reading that wants to be told rather than stopped – as does anything running where there is nowhere to exit to, since a freestanding exit is a halt with no supervisor to notice it.

lines

lines(r: *Reader) -> Lines

A cursor over a file or a pipe: \n ends a line, and a trailing \r is trimmed.

lines_ending

lines_ending(r: *Reader, ending: LineEnding) -> Lines

stdin

stdin() -> FdReader

try_line_text

try_line_text(b: []const u8) -> Result[string, Utf8Error]

The same conversion, reporting rather than stopping.

It exists so that the choice is the caller’s without their having to drop to Reader and re-implement line splitting to get it. Which of the two is right is a property of where the bytes came from, and the library cannot know that – so both are here and the caller says which.

Types

BytesReader

struct BytesReader
    bytes: []const u8
    at: usize
    most: usize

A reader over bytes already in hand.

It borrows what it reads rather than copying it, so a reader over a large buffer costs three words. What that asks of a caller is that the bytes outlive the reader, which is the ordinary rule for a slice and needs no special mention beyond this one.

MemberSignatureDescription
remainingremaining(self) -> usize

BytesWriter

struct BytesWriter
    bytes: Buf[u8]

A writer that keeps what it is given.

The bytes are a Buf[u8], so the sink grows to fit and view hands back everything written so far. A caller wanting text calls from_utf8 on that view, and gets to decide what an ill-formed sequence means rather than being stopped – which is right here, because whatever wrote those bytes is the thing under suspicion.

MemberSignatureDescription
viewview(self) -> []const u8Everything written so far.
lenlen(self) -> usize
is_emptyis_empty(self) -> bool
clearclear(*self)Forget what was written, keeping the storage – so a loop that measures one rendering after another allocates once.

FdReader

struct FdReader
    fd: int
    bad: bool

A reader over a file descriptor – one of the two places a freestanding target has to substitute its own body, the other being putbytes. Swap this for a read syscall and the whole surface above it is unchanged.

LineEnding

enum LineEnding
    Lf
    CrOrLf

What ends a line for a given cursor.

Two policies rather than three: there is no CRLF-only setting, because a reader strict about which of the two a sender used has a protocol rather than a line, and would want the bytes.

Lines

struct Lines
    src: *Reader
    chunk: []u8
    have: []const u8
    at: usize
    held: &Buf[u8]
    done: bool
    ending: LineEnding
    owed_lf: bool

A cursor that yields one line at a time.

It borrows its reader rather than owning it, which is not a preference: a for loop iterates a copy of the iterator, so a Lines holding an FdReader by value would latch its failure on a copy the caller cannot reach, and failed would be decorative. Holding a *Reader leaves the reader in the caller’s hands, which is exactly what makes r.failed() answerable after the loop has ended. It costs a line at the call site – the reader has to be named – and buys the only question worth asking once reading has stopped.

It scans the buffer in place and copies nothing for a line that fits in one read; only a line that spans two reads is gathered, into a Buf[u8] that is then reused, so the cost is proportional to the input rather than to the input times the number of refills.

What it scans is the slice read handed back, not the one it offered. Those are the same memory for a reader that fills what it was given, and the distinction costs nothing – but it is what lets a reader hand back a view of a buffer of its own, never touching the offered one, and still be read correctly. Taking a length from the answer and the bytes from the offer is how the two could have disagreed, and there is now nowhere for them to.

An empty read ends the cursor for good. That is right for the sources read(2) serves, where zero means end of file and a failure arrives as -1 instead – so nothing is being conflated, and a caller wanting to read again past the end gets None every time rather than a second chance.

MemberSignatureDescription
line_endline_end(self, b: []const u8) -> Option[usize]Where the next line ends in b, which is the whole of what the policy decides.
getlinegetline(*self) -> Option[string]The next line, or nothing once the input has ended.
try_getlinetry_getline(*self) -> Option[Result[string, Utf8Error]]The whole of the line-finding, so that the two forms cannot come to disagree about where a line ends.
held_lineheld_line(*self) -> Result[string, Utf8Error]
refillrefill(*self)

Traits

Reader

trait Reader: Fallible
    read(*self, into: []u8) -> []const u8

The input half of the byte surface, and the line cursor over it.

A module of its own rather than part of sysl, because nothing the language desugars onto reads: a program that never takes input never needs one of these names, and one that does says so with an import. The output half stays where it is for the opposite reason – print is a keyword, and what it desugars onto cannot be behind an import a program has not written.

Reader is here for the same reason Writer is: a program that reads has to name what it reads from, and naming it once is what keeps two mechanisms – and so two buffers, and bytes arriving in the wrong order – from growing. It is Writer turned around, down to the Fallible latch both require, so a source that cannot fail says nothing about failing.

The latch is required rather than declared, and that is what lets one type be a reader and a writer at once: a trait’s members become the implementing type’s, so two traits each declaring a failed could not both be implemented for one file. Sharing Fallible is what makes an open file writable at all (sysl.fs).

read hands back the prefix of into that was filled rather than a count, because a slice already is a count and a pointer: the length is got.len and there is no way to be handed one and forget to apply it to the other. Returning empty says end of input and nothing more – whether input ended badly is what failed is for, and the two questions are separate because a caller that does not care should not have to ask.

A TERMINAL DOES NOT END A LINE THE WAY A FILE DOES, and the failure used to be silent. A terminal sends a bare \r when Enter is pressed, so a cursor splitting on \n never sees a line at all: it waits forever, prints nothing, and looks hung, with no diagnostic to grep for, because nothing has gone wrong – the line has not ended.

console_lines is the answer, and lines is unchanged. The policy is the caller’s because the right one is a property of where the bytes came from, which the library cannot know: a pipe carrying \r in the middle of a line means a \r, and a console pressing Enter means a line. So a file or a pipe keeps LF, and a console asks for CR, LF and CRLF alike.

Echo and editing are not covered here, and they are the rest of what a console needs. A terminal in raw mode shows nothing as it is typed, and with no echo a mistake cannot be corrected. Neither is a line cursor’s business – one is about the terminal and the other about the line – and both still have to come from somewhere. That somewhere is sysl.term.edit, which is this trait’s other consumer: it edits where nothing else will, and answers whole lines the same way a Lines does – an Iterate whose Item is string – so the two are interchangeable at a call site, as a *Iterate[string]. Which of them a program wants is decided by sysl.posix.tty.raw on a host, and by there being no line discipline at all on a board.

MemberSignatureDescription
readread(*self, into: []u8) -> []const u8

Implementations

Fallible for BytesReader

impl Fallible for BytesReader

Nothing here can fail: the bytes are already in hand, and running out of them is the end of the input rather than a fault. The block is still written, because a required trait is required whether or not there is anything to say – Fallible.failed has a default answer and no default membership.

Fallible for BytesWriter

impl Fallible for BytesWriter

Memory does not go away, so there is nothing to latch. As above, the block is membership rather than content.

Fallible for FdReader

impl Fallible for FdReader

Iterate for Lines

impl Iterate for Lines

Reader for BytesReader

impl Reader for BytesReader

Reader for FdReader

impl Reader for FdReader

Writer for BytesWriter

impl Writer for BytesWriter

Search

Esc
to navigate to open Esc to close