~sircmpwn/hare

ed762a228e72661c590da68177ae1a3011853915 — Autumn! 8 months ago 3601154
strio,bufio: merge memstream implementation into memio

- memio functions will now error instead of aborting
- renames bufio::{buffered,bufstream} to bufio::{new,stream}
- removes truncate()

Signed-off-by: Autumn! <autumnull@posteo.net>
88 files changed, 1300 insertions(+), 1401 deletions(-)

M bufio/README
D bufio/memstream.ha
M bufio/scanner.ha
A bufio/scanner_test+test.ha
R bufio/{buffered.ha => stream.ha}
A bufio/stream_test+test.ha
M cmd/hare/schedule.ha
M cmd/harec/context.ha
M cmd/harec/gen.ha
M cmd/harec/main.ha
M cmd/haredoc/docstr.ha
M cmd/haredoc/hare.ha
M cmd/haredoc/html.ha
M cmd/haredoc/main.ha
M cmd/haredoc/tty.ha
M cmd/haredoc/util.ha
M crypto/aes/+test/gcm.ha
M crypto/aes/ctr+test.ha
M crypto/argon2/argon2.ha
M crypto/authenc.ha
M crypto/bcrypt/base64.ha
M crypto/bcrypt/bcrypt.ha
M crypto/blake2b/+test.ha
M crypto/chacha/+test.ha
M crypto/chachapoly/chachapoly.ha
M crypto/chachapoly/encryption+test.ha
M crypto/rsa/keys.ha
M crypto/salsa/+test.ha
M encoding/base32/base32.ha
M encoding/base64/base64.ha
M encoding/hex/hex.ha
M encoding/pem/+test.ha
M encoding/pem/pem.ha
M fmt/fmt.ha
M format/ini/+test.ha
M format/tar/reader.ha
M glob/glob.ha
M hare/lex/+test.ha
M hare/lex/lex.ha
M hare/module/context.ha
M hare/module/manifest.ha
M hare/module/scan.ha
M hare/parse/+test/ident_test.ha
M hare/parse/+test/loc.ha
M hare/parse/+test/roundtrip.ha
M hare/parse/+test/unit_test.ha
M hare/parse/ident.ha
M hare/parse/parse.ha
M hare/types/+test.ha
M hare/unit/+test.ha
M hare/unparse/decl.ha
M hare/unparse/ident.ha
M hare/unparse/import.ha
M hare/unparse/type.ha
M hash/siphash/+test.ha
A memio/README
R strio/ops.ha => memio/ops.ha
A memio/stream.ha
M mime/system.ha
M net/ip/ip.ha
M net/uri/fmt.ha
M net/uri/parse.ha
M net/uri/query.ha
M os/+freebsd/stdfd.ha
M os/+linux/stdfd.ha
M regex/regex.ha
M scripts/gen-stdlib
M scripts/install-mods
M shlex/+test.ha
M shlex/escape.ha
M shlex/split.ha
M stdlib.mk
M strings/template/template.ha
D strio/README
D strio/stream.ha
M temp/+freebsd.ha
M temp/+linux.ha
M test/+test.ha
M time/chrono/timezone.ha
M time/chrono/tzdb.ha
M time/date/format.ha
M time/date/parse.ha
M unix/hosts/hosts.ha
M unix/hosts/test+test.ha
M unix/passwd/group.ha
M unix/passwd/passwd.ha
M unix/resolvconf/load.ha
M uuid/uuid.ha
M bufio/README => bufio/README +12 -25
@@ 1,30 1,17 @@
bufio provides [[io::stream]] implementations which provide buffered I/O
bufio provides an [[io::stream]] implementation which provides buffered I/O
support, as well as scanner utility functions which pair well with buffered
streams for optimal efficiency.

Two streams are provided which can read from or write to byte slices. [[fixed]]
uses a caller-supplied statically-allocated buffer for storage, producing an
[[io::stream]] which reads from or writes to this buffer. In effect, this allows
the caller to statically allocate a byte array, then produce an [[io::stream]]
which writes to or reads from it. [[dynamic]] is similar, but it uses a
bufio-managed dynamically allocated buffer. This creates an [[io::stream]] which
efficiently soaks up writes into a dynamically allocated byte slice.
A [[stream]] is used to batch read and write operations against an underlying
stream. The caller may use small, frequent read and write operations, which
bufio will batch into larger, less frequent reads and writes. The caller must
supply either one or two temporary buffers for reading and/or writing, which
bufio will use to store future reads, or pending writes, as necessary. This
improves performance when many small reads or writes would be inefficient, such
as when I/O operations require syscalls or network transmissions.  Buffered
streams also support an "[[unread]]" operation, which allows you to "look-ahead"
at future data without consuming it from the stream.

Both [[fixed]] and [[dynamic]] provide access to the underlying buffer via
[[buffer]]. The user may also call [[reset]], which empties the buffer but does
not free the underlying storage, allowing the user to re-use the same buffer
for many operations.

A third stream implementation, [[buffered]], is used to batch read and write
operations against an underlying stream. The caller may use small, frequent read
and write operations, which bufio will batch into larger, less frequent reads
and writes. The caller must supply either one or two temporary buffers for
reading and/or writing, which bufio will use to store future reads, or pending
writes, as necessary. This improves performance when many small reads or writes
would be inefficient, such as when I/O operations require syscalls or network
transmissions.  Buffered streams also support an "[[unread]]" operation, which
allows you to "look-ahead" at future data without consuming it from the stream.

Finally, bufio provides several utilities for "scanning" streams, namely
Additionally, bufio provides several utilities for "scanning" streams, namely
[[scantok]] et al, which require small, frequent reads, or take advantage of
look-ahead, and thus are most efficient when paired with a [[buffered]] stream.
look-ahead, and thus are most efficient when paired with a bufio [[stream]].

D bufio/memstream.ha => bufio/memstream.ha +0 -299
@@ 1,299 0,0 @@
// License: MPL-2.0
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Bor Grošelj Simić <bor.groseljsimic@telemach.net>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use bytes;
use io;
use strings;
use errors;

export type memstream = struct {
	stream: io::stream,
	buf: []u8,
	pos: size,
};

const memstream_vt_r: io::vtable = io::vtable {
	seeker = &seek,
	copier = &copy,
	reader = &read,
	...
};

const fixed_vt_w: io::vtable = io::vtable {
	seeker = &seek,
	copier = &copy,
	writer = &fixed_write,
	...
};

const fixed_vt_rw: io::vtable = io::vtable {
	seeker = &seek,
	copier = &copy,
	reader = &read,
	writer = &fixed_write,
	...
};

// Creates a stream for a fixed, caller-supplied buffer. All fixed streams are
// seekable; seeking a write stream will cause subsequent writes to overwrite
// existing contents of the buffer. The program aborts if writes would exceed
// the buffer's capacity. The stream doesn't have to be closed.
export fn fixed(in: []u8, mode: io::mode) memstream = {
	let s = memstream {
		stream = &memstream_vt_r,
		buf = in,
		pos = 0,
	};
	if (mode & io::mode::RDWR == io::mode::RDWR) {
		s.stream = &fixed_vt_rw;
	} else if (mode & io::mode::WRITE == io::mode::WRITE) {
		s.stream = &fixed_vt_w;
	} else if (mode & io::mode::READ == io::mode::READ) {
		s.stream = &memstream_vt_r;
	};
	return s;
};

fn fixed_write(s: *io::stream, buf: const []u8) (size | io::error) = {
	if (len(buf) == 0) {
		return 0z;
	};
	let s = s: *memstream;
	if (s.pos >= len(s.buf)) {
		abort("bufio::fixed buffer exceeded");
	};
	const n = if (len(buf) > len(s.buf[s.pos..])) {
		yield len(s.buf[s.pos..]);
	} else {
		yield len(buf);
	};
	s.buf[s.pos..s.pos+n] = buf[..n];
	s.pos += n;
	return n;
};

const dynamic_vt_w: io::vtable = io::vtable {
	seeker = &seek,
	copier = &copy,
	writer = &dynamic_write,
	closer = &dynamic_close,
	...
};

const dynamic_vt_rw: io::vtable = io::vtable {
	seeker = &seek,
	copier = &copy,
	reader = &read,
	writer = &dynamic_write,
	closer = &dynamic_close,
	...
};

// Creates an [[io::stream]] which dynamically allocates a buffer to store
// writes into. Subsequent reads will consume the buffered data. Upon failure to
// allocate sufficient memory to store writes, the program aborts.
//
// Calling [[io::close]] on this stream will free the buffer. If a stream's data
// is transferred via [[buffer]], the stream shouldn't be closed as long as the
// data is used.
export fn dynamic(mode: io::mode) memstream = dynamic_from([], mode);

// Like [[dynamic]], but takes an existing slice as input. Writes are appended
// to it and reads consume bytes from the initial buffer, plus any additional
// writes. Like [[dynamic]], calling [[io::close]] will free the buffer.
export fn dynamic_from(in: []u8, mode: io::mode) memstream = {
	let s = memstream {
		stream = &memstream_vt_r,
		buf = in,
		pos = 0,
	};
	if (mode & io::mode::RDWR == io::mode::RDWR) {
		s.stream = &dynamic_vt_rw;
	} else if (mode & io::mode::WRITE == io::mode::WRITE) {
		s.stream = &dynamic_vt_w;
	} else if (mode & io::mode::READ == io::mode::READ) {
		s.stream = &memstream_vt_r;
	};
	return s;
};

// Returns the current buffer of a [[fixed]] or [[dynamic]] stream.
export fn buffer(in: *memstream) []u8 = {
	return in.buf;
};

// Resets the dynamic buffer's length to zero, but keeps the allocated memory
// around for future writes.
export fn reset(in: *memstream) void = {
	in.pos = 0;
	in.buf = in.buf[..0];
};

// Truncates the dynamic buffer, freeing memory associated with it and setting
// its length to zero.
export fn truncate(in: *memstream) (void | errors::unsupported) = {
	in.pos = 0;
	delete(in.buf[..]);
};

// Reads data from a [[dynamic]] or [[fixed]] stream and returns a slice
// borrowed from the internal buffer.
export fn borrowedread(st: *memstream, amt: size) ([]u8 | io::EOF) = {
	if (len(st.buf) - st.pos < amt) {
		return io::EOF;
	};
	let buf = st.buf[st.pos..st.pos + amt];
	st.pos += len(buf);
	return buf;
};

fn dynamic_write(s: *io::stream, buf: const []u8) (size | io::error) = {
	let s = s: *memstream;
	insert(s.buf[s.pos], buf...);
	s.pos += len(buf);
	return len(buf);
};

fn dynamic_close(s: *io::stream) (void | io::error) = {
	const s = s: *memstream;
	free(s.buf);
};

fn read(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
	let s = s: *memstream;
	if (len(s.buf) == s.pos && len(buf) != 0) {
		return io::EOF;
	};
	const n = if (len(s.buf) - s.pos < len(buf)) {
		yield len(s.buf) - s.pos;
	} else {
		yield len(buf);
	};
	assert(s.pos + n <= len(s.buf));
	buf[..n] = s.buf[s.pos..s.pos + n];
	s.pos += n;
	return n;
};

fn seek(
	s: *io::stream,
	off: io::off,
	w: io::whence
) (io::off | io::error) = {
	let s = s: *memstream;
	switch (w) {
	case io::whence::SET =>
		if (len(s.buf) < off: size) {
			abort("invalid offset");
		};
		s.pos = off: size;
	case io::whence::CUR =>
		if (s.pos + off: size > len(s.buf)) {
			abort("invalid offset");
		};
		s.pos += off: size;
	case io::whence::END =>
		if (len(s.buf) - (-off): size < len(s.buf)) {
			abort("invalid offset");
		};
		s.pos = len(s.buf) - (-off): size;
	};
	return s.pos: io::off;
};

fn copy(dest: *io::stream, src: *io::stream) (size | io::error) = {
	if (src.reader != &read || src.writer == null) {
		return errors::unsupported;
	};
	let src = src: *memstream;
	return (dest.writer: *io::writer)(dest, src.buf[src.pos..]);
};

@test fn dynamic() void = {
	let s = dynamic(io::mode::RDWR);
	assert(io::writeall(&s, [1, 2, 3]) as size == 3);
	assert(bytes::equal(buffer(&s), [1, 2, 3]));
	assert(io::writeall(&s, [4, 5]) as size == 2);
	assert(bytes::equal(buffer(&s), [1, 2, 3, 4, 5]));
	let buf: [2]u8 = [0...];
	assert(io::seek(&s, 0, io::whence::SET) as io::off == 0: io::off);
	assert(io::read(&s, buf[..]) as size == 2 && bytes::equal(buf, [1, 2]));
	assert(io::read(&s, buf[..]) as size == 2 && bytes::equal(buf, [3, 4]));
	assert(io::read(&s, buf[..]) as size == 1 && buf[0] == 5);
	assert(io::read(&s, buf[..]) is io::EOF);
	assert(io::writeall(&s, [6, 7, 8]) as size == 3);
	assert(bytes::equal(buffer(&s), [1, 2, 3, 4, 5, 6, 7, 8]));
	reset(&s);
	assert(len(buffer(&s)) == 0);
	assert(io::writeall(&s, [1, 2, 3]) as size == 3);
	assert(truncate(&s) is void);
	assert(len(buffer(&s)) == 0);

	let sl: []u8 = alloc([1, 2, 3]);
	let s = dynamic_from(sl, io::mode::WRITE);
	assert(io::writeall(&s, [0, 0]) as size == 2);
	assert(io::seek(&s, 0, io::whence::END) as io::off == 5: io::off);
	assert(io::writeall(&s, [4, 5, 6]) as size == 3);
	assert(bytes::equal(buffer(&s), [0, 0, 1, 2, 3, 4, 5, 6]));
	assert(io::read(&s, buf[..]) as io::error is errors::unsupported);
	io::close(&s)!;

	sl = alloc([1, 2]);
	let s = dynamic_from(sl, io::mode::READ);
	assert(io::read(&s, buf[..1]) as size == 1 && buf[0] == 1);
	assert(io::seek(&s, 1, io::whence::CUR) as io::off == 2: io::off);
	assert(io::read(&s, buf[..]) is io::EOF);
	assert(io::write(&s, [1, 2]) as io::error is errors::unsupported);
	io::close(&s)!;
	assert(io::writeall(&s, [1, 2]) as io::error is errors::unsupported);
	io::close(&s)!;

	let in: [6]u8 = [0, 1, 2, 3, 4, 5];
	let source = dynamic_from(in, io::mode::READ);
	let sink = dynamic(io::mode::WRITE);
	io::copy(&sink, &source)!;
	assert(bytes::equal(in, buffer(&sink)));

	let in: [6]u8 = [0, 1, 2, 3, 4, 5];
	let source = dynamic_from(in, io::mode::READ);
	const borrowed = borrowedread(&source, len(in)-1) as []u8;
	assert(bytes::equal(borrowed, [0, 1, 2, 3, 4]));
	let source = dynamic_from(in, io::mode::READ);
	const borrowed = borrowedread(&source, len(in)) as []u8;
	assert(bytes::equal(borrowed, [0, 1, 2, 3, 4, 5]));
	let source = dynamic_from(in, io::mode::READ);
	assert(borrowedread(&source, len(in)+1) is io::EOF);
};

@test fn fixed() void = {
	let buf: [1024]u8 = [0...];
	let stream = fixed(buf, io::mode::WRITE);
	defer io::close(&stream)!;

	let n = 0z;
	n += io::writeall(&stream, strings::toutf8("hello ")) as size;
	n += io::writeall(&stream, strings::toutf8("world")) as size;
	assert(bytes::equal(buf[..n], strings::toutf8("hello world")));
	assert(io::seek(&stream, 6, io::whence::SET) as io::off == 6: io::off);
	io::writeall(&stream, strings::toutf8("asdf")) as size;
	assert(bytes::equal(buf[..n], strings::toutf8("hello asdfd")));

	let out: [2]u8 = [0...];
	let s = fixed([1u8, 2u8], io::mode::READ);
	defer io::close(&s)!;
	assert(io::read(&s, out[..1]) as size == 1 && out[0] == 1);
	assert(io::seek(&s, 1, io::whence::CUR) as io::off == 2: io::off);
	assert(io::read(&s, buf[..]) is io::EOF);
	assert(io::writeall(&s, [1, 2]) as io::error is errors::unsupported);

	let in: [6]u8 = [0, 1, 2, 3, 4, 5];
	let out: [6]u8 = [0...];
	let source = fixed(in, io::mode::READ);
	let sink = fixed(out, io::mode::WRITE);
	io::copy(&sink, &source)!;
	assert(bytes::equal(in, out));

	assert(io::write(&sink, [])! == 0);
};

M bufio/scanner.ha => bufio/scanner.ha +0 -118
@@ 212,16 212,6 @@ export fn scan_rune(
	};
};

@test fn scan_rune() void = {
	let in = fixed(strings::toutf8("1234"), io::mode::READ);
	let scan = newscanner(&in, 4);
	assert(scan_rune(&scan) == '1', "expected '1'");
	assert(scan_rune(&scan) == '2', "expected '2'");
	assert(scan_rune(&scan) == '3', "expected '3'");
	assert(scan_rune(&scan) == '4', "expected '4'");
	finish(&scan);
};

// Scans a string of text from a [[scanner]] up to some delimiter. The return
// value is borrowed from the internal scanner buffer, which is invalidated
// during subsequent operations which use this scanner.


@@ 335,111 325,3 @@ export fn scanrune(
		return utf8::invalid;
	};
};

@test fn scanbyte() void = {
	let buf = fixed([1, 3, 3, 7], io::mode::READ);

	assert(scanbyte(&buf) as u8 == 1);
	assert(scanbyte(&buf) as u8 == 3);
	assert(scanbyte(&buf) as u8 == 3);
	assert(scanbyte(&buf) as u8 == 7);
	assert(scanbyte(&buf) is io::EOF);
};

@test fn scantok() void = {
	let buf = fixed([1, 3, 4, 5, 3, 7], io::mode::READ);

	let tok = scantok(&buf, 4) as []u8;
	defer free(tok);
	assert(bytes::equal(tok, [1, 3]));

	let tok = scantok(&buf, 7) as []u8;
	defer free(tok);
	assert(bytes::equal(tok, [5, 3]));

	assert(scantok(&buf, 1) is io::EOF);
};

@test fn scanline() void = {
	let helloworld = strings::toutf8("hello\nworld");
	let buf = fixed(helloworld, io::mode::READ);

	let line = scanline(&buf) as []u8;
	defer free(line);
	assert(bytes::equal(line, strings::toutf8("hello")));

	let line = scanline(&buf) as []u8;
	defer free(line);
	assert(bytes::equal(line, strings::toutf8("world")));

	assert(scanline(&buf) is io::EOF);
};

@test fn scanrune() void = {
	let in = fixed([
		0xE3, 0x81, 0x93, 0xE3, 0x82, 0x93, 0xE3, 0x81,
		0xAB, 0xE3, 0x81, 0xA1, 0xE3, 0x81, 0xAF, 0x00,
	], io::mode::READ);

	const expected: [_](rune | utf8::invalid | io::EOF | io::error) = [
		'こ', 'ん', 'に', 'ち', 'は', '\0', io::EOF,
	];
	for (let i = 0z; i < len(expected); i += 1) {
		let want = expected[i];

		match (scanrune(&in)) {
		case let r: rune =>
			assert(want is rune && want as rune == r);
		case io::EOF =>
			assert(want is io::EOF);
		case =>
			abort();
		};
	};
};

@test fn scan_rune() void = {
	let in = fixed(strings::toutf8("hello"), io::mode::READ);
	let scanner = newscanner(&in, 32);

	const expected: [_](rune | utf8::invalid | io::EOF | io::error) = [
		'h', 'e', 'l', 'l', 'o', io::EOF,
	];
	for (let i = 0z; i < len(expected); i += 1) {
		let want = expected[i];

		match (scan_rune(&scanner)) {
		case let r: rune =>
			assert(want is rune && want as rune == r);
		case io::EOF =>
			assert(want is io::EOF);
		case =>
			abort();
		};
	};
};

@test fn scan_rune_cutoff() void = {
	let in = fixed([
		'a', 0xE3,
	], io::mode::READ);
	let scanner = newscanner(&in, 32);

	const expected: [_](rune | utf8::invalid | io::EOF | io::error) = [
		'a', utf8::invalid,
	];
	for (let i = 0z; i < len(expected); i += 1) {
		let want = expected[i];

		match (scan_rune(&scanner)) {
		case let r: rune =>
			assert(want is rune && want as rune == r);
		case io::EOF =>
			assert(want is io::EOF);
		case utf8::invalid =>
			assert(want is utf8::invalid);
		case =>
			abort();
		};
	};
};

A bufio/scanner_test+test.ha => bufio/scanner_test+test.ha +113 -0
@@ 0,0 1,113 @@
use bytes;
use encoding::utf8;
use io;
use memio;
use strings;

@test fn scanbyte() void = {
	let buf = memio::fixed([1, 3, 3, 7]);

	assert(scanbyte(&buf) as u8 == 1);
	assert(scanbyte(&buf) as u8 == 3);
	assert(scanbyte(&buf) as u8 == 3);
	assert(scanbyte(&buf) as u8 == 7);
	assert(scanbyte(&buf) is io::EOF);
};

@test fn scantok() void = {
	let buf = memio::fixed([1, 3, 4, 5, 3, 7]);

	let tok = scantok(&buf, 4) as []u8;
	defer free(tok);
	assert(bytes::equal(tok, [1, 3]));

	let tok = scantok(&buf, 7) as []u8;
	defer free(tok);
	assert(bytes::equal(tok, [5, 3]));

	assert(scantok(&buf, 1) is io::EOF);
};

@test fn scanline() void = {
	let helloworld = strings::toutf8("hello\nworld");
	let buf = memio::fixed(helloworld);

	let line = scanline(&buf) as []u8;
	defer free(line);
	assert(bytes::equal(line, strings::toutf8("hello")));

	let line = scanline(&buf) as []u8;
	defer free(line);
	assert(bytes::equal(line, strings::toutf8("world")));

	assert(scanline(&buf) is io::EOF);
};

@test fn scanrune() void = {
	let in = memio::fixed([
		0xE3, 0x81, 0x93, 0xE3, 0x82, 0x93, 0xE3, 0x81,
		0xAB, 0xE3, 0x81, 0xA1, 0xE3, 0x81, 0xAF, 0x00,
	]);

	const expected: [_](rune | utf8::invalid | io::EOF | io::error) = [
		'こ', 'ん', 'に', 'ち', 'は', '\0', io::EOF,
	];
	for (let i = 0z; i < len(expected); i += 1) {
		let want = expected[i];

		match (scanrune(&in)) {
		case let r: rune =>
			assert(want is rune && want as rune == r);
		case io::EOF =>
			assert(want is io::EOF);
		case =>
			abort();
		};
	};
};

@test fn scan_rune() void = {
	let in = memio::fixed(strings::toutf8("hello"));
	let scanner = newscanner(&in, 32);

	const expected: [_](rune | utf8::invalid | io::EOF | io::error) = [
		'h', 'e', 'l', 'l', 'o', io::EOF,
	];
	for (let i = 0z; i < len(expected); i += 1) {
		let want = expected[i];

		match (scan_rune(&scanner)) {
		case let r: rune =>
			assert(want is rune && want as rune == r);
		case io::EOF =>
			assert(want is io::EOF);
		case =>
			abort();
		};
	};
};

@test fn scan_rune_cutoff() void = {
	let in = memio::fixed([
		'a', 0xE3,
	]);
	let scanner = newscanner(&in, 32);

	const expected: [_](rune | utf8::invalid | io::EOF | io::error) = [
		'a', utf8::invalid,
	];
	for (let i = 0z; i < len(expected); i += 1) {
		let want = expected[i];

		match (scan_rune(&scanner)) {
		case let r: rune =>
			assert(want is rune && want as rune == r);
		case io::EOF =>
			assert(want is io::EOF);
		case utf8::invalid =>
			assert(want is utf8::invalid);
		case =>
			abort();
		};
	};
};

R bufio/buffered.ha => bufio/stream.ha +39 -152
@@ 9,26 9,26 @@ use errors;
use io;
use strings;

const buffered_vtable_r: io::vtable = io::vtable {
	closer = &buffered_close_static,
	reader = &buffered_read,
const vtable_r: io::vtable = io::vtable {
	closer = &close_static,
	reader = &read,
	...
};

const buffered_vtable_w: io::vtable = io::vtable {
	closer = &buffered_close_static,
	writer = &buffered_write,
const vtable_w: io::vtable = io::vtable {
	closer = &close_static,
	writer = &write,
	...
};

const buffered_vtable_rw: io::vtable = io::vtable {
	closer = &buffered_close_static,
	reader = &buffered_read,
	writer = &buffered_write,
const vtable_rw: io::vtable = io::vtable {
	closer = &close_static,
	reader = &read,
	writer = &write,
	...
};

export type bufstream = struct {
export type stream = struct {
	stream: io::stream,
	source: io::handle,
	rbuffer: []u8,


@@ 53,29 53,29 @@ export type bufstream = struct {
//
// 	let rbuf: [os::BUFSIZ]u8 = [0...];
// 	let wbuf: [os::BUFSIZ]u8 = [0...];
// 	let buffered = bufio::buffered(source, rbuf, wbuf);
export fn buffered(
// 	let buffered = bufio::init(source, rbuf, wbuf);
export fn init(
	src: io::handle,
	rbuf: []u8,
	wbuf: []u8,
) bufstream = {
) stream = {
	static let flush_default = ['\n': u8];

	let stream =
	let st =
		if (len(rbuf) != 0 && len(wbuf) != 0) {
			assert(rbuf: *[*]u8 != wbuf: *[*]u8,
				"Cannot use bufio::buffered with same buffer for reads and writes");
			yield &buffered_vtable_rw;
				"Cannot use same buffer for reads and writes");
			yield &vtable_rw;
		} else if (len(rbuf) != 0) {
			yield &buffered_vtable_r;
			yield &vtable_r;
		} else if (len(wbuf) != 0) {
			yield &buffered_vtable_w;
			yield &vtable_w;
		} else {
			abort("Must provide at least one buffer to bufio::buffered");
			abort("Must provide at least one buffer");
		};

	return bufstream {
		stream = stream,
	return stream {
		stream = st,
		source = src,
		rbuffer = rbuf,
		wbuffer = wbuf,


@@ 89,10 89,10 @@ export fn buffered(
export fn flush(s: io::handle) (void | io::error) = {
	let s = match (s) {
	case let st: *io::stream =>
		if (st.writer != &buffered_write) {
		if (st.writer != &write) {
			return errors::unsupported;
		};
		yield st: *bufstream;
		yield st: *stream;
	case =>
		return errors::unsupported;
	};


@@ 109,10 109,10 @@ export fn flush(s: io::handle) (void | io::error) = {
export fn setflush(s: io::handle, b: []u8) void = {
	let s = match (s) {
	case let st: *io::stream =>
		if (st.writer != &buffered_write) {
		if (st.writer != &write) {
			abort("Attempted to set flush bytes on unbuffered stream");
		};
		yield st: *bufstream;
		yield st: *stream;
	case =>
		abort("Attempted to set flush bytes on unbuffered stream");
	};


@@ 130,10 130,10 @@ export fn setflush(s: io::handle, b: []u8) void = {
export fn unread(s: io::handle, buf: []u8) void = {
	let s = match (s) {
	case let st: *io::stream =>
		if (st.reader != &buffered_read) {
		if (st.reader != &read) {
			abort("Attempted unread on unbuffered stream");
		};
		yield st: *bufstream;
		yield st: *stream;
	case =>
		abort("Attempted unread on unbuffered stream");
	};


@@ 150,26 150,26 @@ export fn unreadrune(s: io::handle, rn: rune) void = {
	unread(s, buf);
};

// Returns true if an [[io::handle]] is a [[buffered]] stream.
// Returns true if an [[io::handle]] is a [[stream]].
export fn isbuffered(in: io::handle) bool = {
	match (in) {
	case io::file =>
		return false;
	case let st: *io::stream =>
		return st.reader == &buffered_read || st.writer == &buffered_write;
		return st.reader == &read || st.writer == &write;
	};
};

fn buffered_close_static(s: *io::stream) (void | io::error) = {
	assert(s.closer == &buffered_close_static);
fn close_static(s: *io::stream) (void | io::error) = {
	assert(s.closer == &close_static);
	if (s.writer != null) {
		flush(s: *bufstream)?;
		flush(s: *stream)?;
	};
};

fn buffered_read(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
	assert(s.reader == &buffered_read);
	let s = s: *bufstream;
fn read(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
	assert(s.reader == &read);
	let s = s: *stream;

	if (s.ravail < len(buf) && s.ravail < len(s.rbuffer)) {
		s.rbuffer[..s.ravail] = s.rbuffer[s.rpos..s.rpos + s.ravail];


@@ 193,9 193,9 @@ fn buffered_read(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
	return n;
};

fn buffered_write(s: *io::stream, buf: const []u8) (size | io::error) = {
	assert(s.writer == &buffered_write);
	let s = s: *bufstream;
fn write(s: *io::stream, buf: const []u8) (size | io::error) = {
	assert(s.writer == &write);
	let s = s: *stream;
	let buf = buf;

	let doflush = false;


@@ 231,116 231,3 @@ fn buffered_write(s: *io::stream, buf: const []u8) (size | io::error) = {

	return z;
};

@test fn buffered_read() void = {
	let sourcebuf: []u8 = [1, 3, 3, 7];
	let source = fixed(sourcebuf, io::mode::READ);
	defer io::close(&source)!;

	let rbuf: [1024]u8 = [0...];
	let f = buffered(&source, rbuf, []);
	defer io::close(&f)!;

	let buf: [1024]u8 = [0...];
	assert(io::read(&f, buf[..2]) as size == 2);
	assert(source.pos == len(source.buf), "fixed stream was not fully consumed");
	assert(bytes::equal(buf[..2], [1, 3]));

	assert(io::read(&f, buf[2..]) as size == 2);
	assert(bytes::equal(buf[..4], [1, 3, 3, 7]));
	assert(io::read(&f, buf) is io::EOF);

	let sourcebuf: [32]u8 = [1, 3, 3, 7, 0...];
	let source = fixed(sourcebuf, io::mode::READ);

	let rbuf: [16]u8 = [0...];
	let f = buffered(&source, rbuf, []);
	defer io::close(&f)!;

	let buf: [32]u8 = [0...];
	assert(io::read(&f, buf) as size == 16);
	assert(source.pos == 16);

	assert(io::read(&f, buf[16..]) as size == 16);
	assert(bytes::equal(buf, sourcebuf));
	assert(io::read(&f, buf) is io::EOF);
	assert(source.pos == len(source.buf));
};

@test fn buffered_write() void = {
	// Normal case
	let sink = dynamic(io::mode::WRITE);
	defer io::close(&sink)!;

	let wbuf: [1024]u8 = [0...];
	let f = buffered(&sink, [], wbuf);
	defer io::close(&f)!;

	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(len(buffer(&sink)) == 0);
	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(flush(&f) is void);
	assert(bytes::equal(buffer(&sink), [1, 3, 3, 7, 1, 3, 3, 7]));

	// Test flushing via buffer exhaustion
	let sink = dynamic(io::mode::WRITE);
	defer io::close(&sink)!;

	let wbuf: [4]u8 = [0...];
	let f = buffered(&sink, [], wbuf);

	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(len(buffer(&sink)) == 0);
	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(bytes::equal(buffer(&sink), [1, 3, 3, 7]));
	io::close(&f)!; // Should flush
	assert(bytes::equal(buffer(&sink), [1, 3, 3, 7, 1, 3, 3, 7]));

	// Test flushing via flush characters
	let sink = dynamic(io::mode::WRITE);
	defer io::close(&sink)!;

	let wbuf: [1024]u8 = [0...];
	let f = buffered(&sink, [], wbuf);

	assert(io::writeall(&f, strings::toutf8("hello")) as size == 5);
	assert(len(buffer(&sink)) == 0);
	assert(io::writeall(&f, strings::toutf8(" world!\n")) as size == 8);
	assert(bytes::equal(buffer(&sink), strings::toutf8("hello world!\n")));
};

@test fn unread() void = {
	let rbuf: [8]u8 = [0...];
	let f = buffered(io::zero, rbuf, []);

	let buf: [16]u8 = [42...];
	assert(io::read(&f, buf[..4]) as size == 4);
	assert(buf[0] == 0);
	assert(buf[1] == 0);
	assert(buf[2] == 0);
	assert(buf[3] == 0);
	unread(&f, [1, 2, 3, 4]);

	assert(io::read(&f, buf[..8]) as size == 8);
	assert(buf[0] == 1);
	assert(buf[1] == 2);
	assert(buf[2] == 3);
	assert(buf[3] == 4);
	assert(buf[4] == 0);
	assert(buf[5] == 0);
	assert(buf[6] == 0);
	assert(buf[7] == 0);

	assert(io::read(&f, buf) as size == 8);
	for (let i = 0z; i < 8; i += 1) {
		assert(buf[i] == 0);
	};

	let input: []u8 = [1, 2, 3, 4];
	let f = buffered(&fixed(input, io::mode::READ), rbuf, []);

	assert(io::read(&f, buf) as size == 4);
	unread(&f, [1, 2, 3, 4]);
	assert(io::read(&f, buf) as size == 4);
	assert(io::read(&f, buf) is io::EOF);
};

A bufio/stream_test+test.ha => bufio/stream_test+test.ha +117 -0
@@ 0,0 1,117 @@
use bytes;
use io;
use memio;
use strings;

@test fn read() void = {
	let sourcebuf: []u8 = [1, 3, 3, 7];
	let source = memio::fixed(sourcebuf);
	defer io::close(&source)!;

	let rbuf: [1024]u8 = [0...];
	let f = init(&source, rbuf, []);
	defer io::close(&f)!;

	let buf: [1024]u8 = [0...];
	assert(io::read(&f, buf[..2]) as size == 2);
	assert(source.pos == len(source.buf), "fixed stream was not fully consumed");
	assert(bytes::equal(buf[..2], [1, 3]));

	assert(io::read(&f, buf[2..]) as size == 2);
	assert(bytes::equal(buf[..4], [1, 3, 3, 7]));
	assert(io::read(&f, buf) is io::EOF);

	let sourcebuf: [32]u8 = [1, 3, 3, 7, 0...];
	let source = memio::fixed(sourcebuf);

	let rbuf: [16]u8 = [0...];
	let f = init(&source, rbuf, []);
	defer io::close(&f)!;

	let buf: [32]u8 = [0...];
	assert(io::read(&f, buf) as size == 16);
	assert(source.pos == 16);

	assert(io::read(&f, buf[16..]) as size == 16);
	assert(bytes::equal(buf, sourcebuf));
	assert(io::read(&f, buf) is io::EOF);
	assert(source.pos == len(source.buf));
};

@test fn write() void = {
	// Normal case
	let sink = memio::dynamic();
	defer io::close(&sink)!;

	let wbuf: [1024]u8 = [0...];
	let f = init(&sink, [], wbuf);
	defer io::close(&f)!;

	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(len(memio::buffer(&sink)) == 0);
	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(flush(&f) is void);
	assert(bytes::equal(memio::buffer(&sink), [1, 3, 3, 7, 1, 3, 3, 7]));

	// Test flushing via buffer exhaustion
	let sink = memio::dynamic();
	defer io::close(&sink)!;

	let wbuf: [4]u8 = [0...];
	let f = init(&sink, [], wbuf);

	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(len(memio::buffer(&sink)) == 0);
	assert(io::writeall(&f, [1, 3, 3, 7]) as size == 4);
	assert(bytes::equal(memio::buffer(&sink), [1, 3, 3, 7]));
	io::close(&f)!; // Should flush
	assert(bytes::equal(memio::buffer(&sink), [1, 3, 3, 7, 1, 3, 3, 7]));

	// Test flushing via flush characters
	let sink = memio::dynamic();
	defer io::close(&sink)!;

	let wbuf: [1024]u8 = [0...];
	let f = init(&sink, [], wbuf);

	assert(io::writeall(&f, strings::toutf8("hello")) as size == 5);
	assert(len(memio::buffer(&sink)) == 0);
	assert(io::writeall(&f, strings::toutf8(" world!\n")) as size == 8);
	assert(bytes::equal(memio::buffer(&sink), strings::toutf8("hello world!\n")));
};

@test fn unread() void = {
	let rbuf: [8]u8 = [0...];
	let f = init(io::zero, rbuf, []);

	let buf: [16]u8 = [42...];
	assert(io::read(&f, buf[..4]) as size == 4);
	assert(buf[0] == 0);
	assert(buf[1] == 0);
	assert(buf[2] == 0);
	assert(buf[3] == 0);
	unread(&f, [1, 2, 3, 4]);

	assert(io::read(&f, buf[..8]) as size == 8);
	assert(buf[0] == 1);
	assert(buf[1] == 2);
	assert(buf[2] == 3);
	assert(buf[3] == 4);
	assert(buf[4] == 0);
	assert(buf[5] == 0);
	assert(buf[6] == 0);
	assert(buf[7] == 0);

	assert(io::read(&f, buf) as size == 8);
	for (let i = 0z; i < 8; i += 1) {
		assert(buf[i] == 0);
	};

	let input: []u8 = [1, 2, 3, 4];
	let f = init(&memio::fixed(input), rbuf, []);

	assert(io::read(&f, buf) as size == 4);
	unread(&f, [1, 2, 3, 4]);
	assert(io::read(&f, buf) as size == 4);
	assert(io::read(&f, buf) is io::EOF);
};

M cmd/hare/schedule.ha => cmd/hare/schedule.ha +0 -1
@@ 16,7 16,6 @@ use os;
use path;
use shlex;
use strings;
use strio;

fn getenv(var: str) []str = {
	match (os::getenv(var)) {

M cmd/harec/context.ha => cmd/harec/context.ha +2 -2
@@ 1,14 1,14 @@
// License: GPL-3.0
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
use bufio;
use io;
use hare::types;
use hare::unit;
use memio;

type context = struct {
	out: io::handle,
	buf: bufio::memstream,
	buf: memio::stream,
	store: *types::typestore,
	unit: *unit::unit,
	arch: struct {

M cmd/harec/gen.ha => cmd/harec/gen.ha +4 -4
@@ 2,7 2,6 @@
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use bufio;
use fmt;
use hare::ast;
use hare::lex;


@@ 12,6 11,7 @@ use hare::types::{builtin};
use hare::unit;
use hare::unit::{object_kind};
use io;
use memio;
use os;
use strings;



@@ 19,7 19,7 @@ fn gen(out: io::handle, store: *types::typestore, unit: *unit::unit) void = {
	// TODO: context_init
	let ctx = context {
		out = out,
		buf = bufio::dynamic(io::mode::WRITE),
		buf = memio::dynamic(),
		store = store,
		unit = unit,
		arch = struct {


@@ 77,7 77,7 @@ fn gen_func(ctx: *context, decl: *unit::decl) void = {
	// on-demand at the start of the function, rather than spread out
	// through the body. This is more reliable on qbe's ARM backend, and
	// generates better IL besides.
	bufio::reset(&ctx.buf);
	memio::reset(&ctx.buf);

	fmt::fprintln(&ctx.buf, mklabel(ctx, "body"))!;



@@ 90,7 90,7 @@ fn gen_func(ctx: *context, decl: *unit::decl) void = {
		};
	};

	io::writeall(ctx.out, bufio::buffer(&ctx.buf))!;
	io::writeall(ctx.out, memio::buffer(&ctx.buf))!;
	fmt::fprintln(ctx.out, "}\n")!;
};


M cmd/harec/main.ha => cmd/harec/main.ha +1 -1
@@ 70,7 70,7 @@ export fn main() void = {
		};
		defer io::close(input)!;
		static let buf: [os::BUFSIZ]u8 = [0...];
		let bufin = bufio::buffered(input, buf, []);
		let bufin = bufio::init(input, buf, []);
		defer io::close(&bufin)!;

		let lexer = lex::init(&bufin, cmd.args[i]);

M cmd/haredoc/docstr.ha => cmd/haredoc/docstr.ha +15 -15
@@ 11,8 11,8 @@ use fmt;
use hare::ast;
use hare::parse;
use io;
use memio;
use strings;
use strio;

type paragraph = void;
type text = str;


@@ 28,14 28,14 @@ type docstate = enum {
};

type parser = struct {
	src: bufio::bufstream,
	src: bufio::stream,
	state: docstate,
};

fn parsedoc(in: io::handle) parser = {
	static let buf: [4096]u8 = [0...];
	return parser {
		src = bufio::buffered(in, buf[..], []),
		src = bufio::init(in, buf[..], []),
		state = docstate::PARAGRAPH,
	};
};


@@ 84,7 84,7 @@ fn scantext(par: *parser) (token | void) = {
		return paragraph;
	};
	// TODO: Collapse whitespace
	const buf = strio::dynamic();
	const buf = memio::dynamic();
	for (true) {
		const rn = match (bufio::scanrune(&par.src)!) {
		case io::EOF => break;


@@ 96,7 96,7 @@ fn scantext(par: *parser) (token | void) = {
			bufio::unreadrune(&par.src, rn);
			break;
		case '\n' =>
			strio::appendrune(&buf, rn)!;
			memio::appendrune(&buf, rn)!;
			const rn = match (bufio::scanrune(&par.src)!) {
			case io::EOF => break;
			case let rn: rune =>


@@ 111,10 111,10 @@ fn scantext(par: *parser) (token | void) = {
				break;
			};
		case =>
			strio::appendrune(&buf, rn)!;
			memio::appendrune(&buf, rn)!;
		};
	};
	let result = strio::string(&buf);
	let result = memio::string(&buf)!;
	if (len(result) == 0) {
		return;
	};


@@ 140,7 140,7 @@ fn scanref(par: *parser) (token | void) = {
		};
	};

	const buf = strio::dynamic();
	const buf = memio::dynamic();
	defer io::close(&buf)!;
	// TODO: Handle invalid syntax here
	for (true) {


@@ 151,12 151,12 @@ fn scanref(par: *parser) (token | void) = {
				bufio::scanrune(&par.src) as rune; // ]
				break;
			case =>
				strio::appendrune(&buf, rn)!;
				memio::appendrune(&buf, rn)!;
			};
		case io::EOF => break;
		};
	};
	let id = parse::identstr(strio::string(&buf)) as ast::ident;
	let id = parse::identstr(memio::string(&buf)!) as ast::ident;
	return id: reference;
};



@@ 183,7 183,7 @@ fn scansample(par: *parser) (token | void) = {
	};

	let cont = true;
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	for (cont) {
		const rn = match (bufio::scanrune(&par.src)!) {
		case io::EOF => break;


@@ 192,9 192,9 @@ fn scansample(par: *parser) (token | void) = {
		};
		switch (rn) {
		case '\n' =>
			strio::appendrune(&buf, rn)!;
			memio::appendrune(&buf, rn)!;
		case =>
			strio::appendrune(&buf, rn)!;
			memio::appendrune(&buf, rn)!;
			continue;
		};



@@ 209,7 209,7 @@ fn scansample(par: *parser) (token | void) = {
				case '\t' =>
					i += 8;
				case '\n' =>
					strio::appendrune(&buf, rn)!;
					memio::appendrune(&buf, rn)!;
					i = 0;
				case =>
					bufio::unreadrune(&par.src, rn);


@@ 220,7 220,7 @@ fn scansample(par: *parser) (token | void) = {
		};
	};

	let buf = strio::string(&buf);
	let buf = memio::string(&buf)!;
	// Trim trailing newlines
	buf = strings::rtrim(buf, '\n');
	return buf: sample;

M cmd/haredoc/hare.ha => cmd/haredoc/hare.ha +0 -1
@@ 11,7 11,6 @@ use hare::unparse;
use io;
use os;
use strings;
use strio;

// Formats output as Hare source code (prototypes)
fn emit_hare(ctx: *context) (void | error) = {

M cmd/haredoc/html.ha => cmd/haredoc/html.ha +12 -13
@@ 7,7 7,6 @@
// (c) 2022 Umar Getagazov <umar@handlerug.me>

// Note: ast::ident should never have to be escaped
use bufio;
use encoding::utf8;
use fmt;
use hare::ast;


@@ 16,12 15,12 @@ use hare::lex;
use hare::module;
use hare::unparse;
use io;
use memio;
use net::ip;
use net::uri;
use os;
use path;
use strings;
use strio;

// Prints a string to an output handle, escaping any of HTML's reserved
// characters.


@@ 52,20 51,20 @@ fn html_escape(out: io::handle, in: str) (size | io::error) = {
};

@test fn html_escape() void = {
	let sink = strio::dynamic();
	let sink = memio::dynamic();
	defer io::close(&sink)!;
	html_escape(&sink, "hello world!")!;
	assert(strio::string(&sink) == "hello world!");
	assert(memio::string(&sink)! == "hello world!");

	let sink = strio::dynamic();
	let sink = memio::dynamic();
	defer io::close(&sink)!;
	html_escape(&sink, "\"hello world!\"")!;
	assert(strio::string(&sink) == "&quot;hello world!&quot;");
	assert(memio::string(&sink)! == "&quot;hello world!&quot;");

	let sink = strio::dynamic();
	let sink = memio::dynamic();
	defer io::close(&sink)!;
	html_escape(&sink, "<hello & 'world'!>")!;
	assert(strio::string(&sink) == "&lt;hello &amp; &apos;world&apos;!&gt;");
	assert(memio::string(&sink)! == "&lt;hello &amp; &apos;world&apos;!&gt;");
};

// Formats output as HTML


@@ 331,7 330,7 @@ fn details(ctx: *context, decl: ast::decl) (void | error) = {
		const trimmed = trim_comment(decl.docs);
		defer free(trimmed);
		const buf = strings::toutf8(trimmed);
		markup_html(ctx, &bufio::fixed(buf, io::mode::READ))?;
		markup_html(ctx, &memio::fixed(buf))?;
	} else {
		fmt::fprintln(ctx.out, "</details>")?;
	};


@@ 632,10 631,10 @@ fn type_html(
	brief: bool,
) (size | io::error) = {
	if (brief) {
		let buf = strio::dynamic();
		let buf = memio::dynamic();
		defer io::close(&buf)!;
		unparse::_type(&buf, indent, _type)?;
		return html_escape(out, strio::string(&buf))?;
		return html_escape(out, memio::string(&buf)!)?;
	};

	// TODO: More detailed formatter which can find aliases nested deeper in


@@ 854,7 853,7 @@ fn breadcrumb(ident: ast::ident) str = {
	if (len(ident) == 0) {
		return "";
	};
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	fmt::fprintf(&buf, "<a href='/'>stdlib</a> » ")!;
	for (let i = 0z; i < len(ident) - 1; i += 1) {
		let ipath = module::identpath(ident[..i+1]);


@@ 862,7 861,7 @@ fn breadcrumb(ident: ast::ident) str = {
		fmt::fprintf(&buf, "<a href='/{}'>{}</a>::", ipath, ident[i])!;
	};
	fmt::fprint(&buf, ident[len(ident) - 1])!;
	return strio::string(&buf);
	return memio::string(&buf)!;
};

const harriet_b64 = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAAK40lEQVRo3u3ZX2xb1R0H8O/NzWIXXGw0xILa1QE6Wk0gMspIESU3WSf2sD/wODFtpFC1Q1Ob0AJpacm5pYVUAxHENK2IUiONaQ/TBIjRFKXNvSHbijSDeaGja5vr/ovHlmIHQ66de+/57iF27Gv7um8TD/glUvzROb9z7jnnnp9/4GU++Ap8iYEeJ6EFA9k9SSlGgkFRFiizs8HgPKWQ33ZFIEgZjiYNSwsECTpxaViJQKDRSUnDSgUBKcjN0mAmEJAclAbtIOCRhiMNOkHAIVl0DRaDQJ6k5xr0gkCGpOuRbhDIkvzUWwi2IbBI8smF4TYEr5C0nzTIIGCQ5N1NgEbaPGaUZD2QgvKw0QxYzviJkSbAZXH8RPQVozSceuDROzw3ciYYFOkdPhE9YxhBwOGlwydGThtkqjHIk/98fOT06wtz3hBMnfh85HTWCAI2p6a+ME7zWCCQU3MfaUkRDBzL/mg0Sa8JcE4Mz/DY4rKui+HTY/cPz9AIBHJm6onhGVbWfS2Yn7F+uXfGYBD4wnGtGXVmLBjwsf5jTYHzpHdUvTDmBYGMw0tT6ucMBLZjfPoLpRnwjLmtvV+UNmlj8Piu3lwzQHu0N5cNBpLj+d5cfxOQH8/3FrYGgrx0lrX3Ok3BA2sVZyttJ2hVe8faFSdqB4F5/vxgu+JodnALYupfitMVDJytcgeKg8HAE3NCKTIQFN1B3tLrBc+k5261blG814OBXOFs6PX+3AREt3T0en8IBC6fvXSkpwmQ3P+1I/DeDgbyvbaP4R02AsFQsu09eIezweCvLWl41wZ2QbFR7YOL/mAwrXYoLoQVBLRzSidcPHkmCBj58Atw9WYA+hVyYksgSMzq5hXy4mNeICjqPbfKt78VAKy0dQQ9Qj59q5dvCEw9dQTKqNy7rL/h7i704d6j92FU/vpUAFASWbcdo+5Tp37VECRDzLirO+ha0tncALjZEWYkbqZNOr0NwPMik7MlHpMqKU+JepDRisxLXcuuIjnfANAaYp77jPxxkvP1XbjMWymHfzOOkqTM1gE5tDszeZKTTqpyD/ABzU7EeZI/c/OlC1Ut0Heet5hkf+nqkKkFxYnu3eQFitIrM1ULXHXEIrtZvsX9o66LUJ7kIWGUl1YtONS2m6RVvnn018XwaUgzFq4gJMl7a+fBLWzXFi8xpKx7+7vKzkTV8Pm7uqm23Or5YflaWwGmRkpt8WKRzdUAZ2+CVTEwNVcDCshmSBbKozhlCz+QLYP+N4et+UEiGr8MqAyAJHnRNmrmYeFPjo7hhkh6dqImhoWYCnSttEKymI/7QenZHBC2MCFIJ+cH7vWh0hulaOjQyHyhBnA2J0qPCUiQLERrpnrhmnsjbQGkGgFOkuQGOoSSqQcFU3guKQfpEWq+UQvqYlcLYHe0wRF0Xi63KKA69eB8QewhKc/atKAWSTkV8oHptigpzjJDsiHI2iRlnHGSUM6SHPWDUCFO0hWuQwJnSXK4QZAhFklCyZHMTtQsOS1TTkAAk+R/0z7wXKE9SroicxepK30knVkfWJfTSA5TdgvqAEk+EphnLYC5og8sbJOikAnSRIcgDbfhkpvuFjQBksd8QGrnF9bDlCDTCzF4vhbS0btJyqhkGVg1XZiCLh1mk2QOSiOgCZK0EinmECI55wOumCApGKVGuojXpdXF82nBAj/jXJykSZIc93WRSpPZImfnKhn3UX8MWZKajEoxXJVyVc3D1bl1dEnK7ZWLgC+G4lmNGdKtJLsUogpkmNNIg5PFFP0HwuKSm3U1Kcj8Sbsq/a2AwkAhcjxPSnGS5AdDlSjL4KGCUGjxrPy6IA++X3m+JZDrWtGmUmPc0wW5653Kdi+B9+QTK65ySTomKe3Buqn+GH1sd0hy4pAopWludQyzs89SJWWeE4mEb42VgwzFB6OC71BLrvEfayWQTu+IjguSorCqvIonq8Fes88qkJTiXLQExNPVIIdn4ueNcSbsd5eX/qP5DpBcy4pdz4id7LIPvVSKasVSXwybhrpyMs+u7FgpSDeyonqYE+qOyKRhc0vq/KrSeYru6mHGQvqy5zWXD2eT58pXD9+CGVCe6Sp0F+mIk/tLQLd9jxvron13k/Pisx2bSQ6Se3y7G+jsTgtSWnO59eT0JsG9ftDy6t05Usoxt0+1eCaZ5/BMFZDX5/Zft50Guf1IUknQGctyOFsNHppc3k5q5ODR0xtesmgbHPY9rLASW8LufjLjHei7K0GSz6+qbgFQVVd+YGezfCO55i2SfP4bVcDtiUVDnzCZGSuy80N1jSD53APVLehYHprUilk6o30vYns/OWreWh2Drq4N/Z351Jzd/8lhbN9iFV80Vf9ErR/RN9uJS/Lk2ZVQt1jFF+F7Lb6GNjUseNcu74WdK6EsPbmhBuiIqLGhoW27jNc6f4QYPn5Yb/G9L0yoz9y+Q5um6OgMAzjQgw5fC0/hytbIfSJJ66ftMewDwi1+cAhAGKnTjpErgxt94ICC5P1IFB0ndxuwD51hfMe3qtMK0vcpY/mxvHsH8BpiUGK+Fs6hZf/tapfdPchHASAGxHwtJDG8dvW1m4aG7uWjVwKIdaDFdwwWwti+ujU5ZU9l3CvQis4OoLoFcwB9Pwg/95KVOTPtXnFtK2JA9UxaPAdErx75zcvZ7PuFZS9CeQFQfCfMtBJbtmd4zctZeebUZh2qDiylf3cPqOqPeVf/7lOntqQBYKleHaQZ7klfhYfHh7bSeXkBRNZXgJzk7B59+bYfjouZFOc/eVAHYuH1vi7yKmLusrHBS2c4/5/vmUA7enyb92ALsFvt9C6+YnXMf9iDcASoasHFughwce+A4DtjFz42gchN1UCSbjuU48MDXXTeenyFiWtaWxTf+WBe1Qn1gz8ORBXnjjvu+FAHdGWv/5XUgfg+uTEykX+8bTSnA1AmfaO4qgdxTF1QzOOb2kZzaQAIVQNTAlAOXlInRnY/txJpAFCrQI4EoPxll/ryN9cl0ToBILykugVXjQHKd3/zoLZ07brV6AEQifsv3jrQsnlV34qlHdcsQw+A1hpgAh33bOu7xnsVoRvuaQDSQF9ywOwUb6DtBgDlFbe4HtJAZP/GyevFm0BLKwD4Uhg9WgCWHvj++o7Nb4aBlXWAhQFgyXVt2LRV+RMQ2wfAly2avx8A2te0tGzdqBLAPsRUzR/kNHD1bcAHSdhHAACqUQ3+jVbgxptiiCTx26M9PQCW1CRBLvBgayewBPvWnTYbAJq4R9GBPdBv9kwsbovF7a+aiAA9APSbb+kB4E+rcypNlD+RJX2PhDFY04UEAHQCQCT8RC68WKAozaQOFwAGVCAGbBtoDWk1LZh7dQA/ARCLoBPoqgEXoOrlGJZMdgJd9T+qL4Lw5FqgvjyR6yx9H8O7nQtJTPX7oh2YXRynuXi8+LrIl/sIm8CVhXjtPOjKCwCANvQAWBatbcEk3ygBLJ5w/nv1qy2ofKxa4CLqjFS+v7Nxqait/L268/N4I7Cp9H1L4s7F3NgHZjoA4KbtaqXM41tyiAMApgejlV+Ka/KLtLq8e9806ZlqQLFJ04xsk4IXECIzx11EgytiBUCp/OofWFMbaQ4KVRW1WpCGIuaDg6waXLYBSFdin2v0uCcqOyhqNAkSomllMK01Lx2evUxt8enLFB8roeXizae6Os2qBwXEm9U302heANUvUyEd/n9Vac3mwFW+qlZ/WcH/ADT9vVqjZ2RdAAAAAElFTkSuQmCC";

M cmd/haredoc/main.ha => cmd/haredoc/main.ha +2 -2
@@ 3,7 3,6 @@
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
// (c) 2022 Sebastian <sebastian@sebsite.pw>
use bufio;
use fmt;
use fs;
use getopt;


@@ 13,6 12,7 @@ use hare::module;
use hare::parse;
use hare::unparse;
use io;
use memio;
use os;
use os::exec;
use path;


@@ 232,7 232,7 @@ export fn main() void = {
// to the ident in the string. For example, this function will parse `rt::abort`
// as a valid identifier.
fn parseident(in: str) (ast::ident | parse::error) = {
	const buf = bufio::fixed(strings::toutf8(in), io::mode::READ);
	const buf = memio::fixed(strings::toutf8(in));
	const lexer = lex::init(&buf, "<string>");
	defer lex::finish(&lexer);
	let ident: []str = []; // TODO: errdefer

M cmd/haredoc/tty.ha => cmd/haredoc/tty.ha +7 -7
@@ 10,9 10,9 @@ use hare::ast::{variadism};
use hare::lex;
use hare::unparse;
use io;
use memio;
use os;
use strings;
use strio;

let firstline: bool = true;



@@ 270,22 270,22 @@ fn prototype_tty(
	// estimate length of prototype to determine if it should span multiple
	// lines
	const linelen = if (len(t.params) == 0) {
		let strm = strio::dynamic();
		let strm = memio::dynamic();
		defer io::close(&strm)!;
		type_tty(&strm, indent, *t.result)?;
		retname = strings::dup(strio::string(&strm));
		retname = strings::dup(memio::string(&strm)!);
		yield 0z; // only use one line if there's no parameters
	} else {
		let strm = strio::dynamic();
		let strm = memio::dynamic();
		defer io::close(&strm)!;
		let linelen = indent * 8 + 5;
		linelen += if (len(t.params) != 0) len(t.params) * 3 - 1 else 0;
		for (let i = 0z; i < len(t.params); i += 1) {
			const param = t.params[i];
			linelen += unparse::_type(&strm, indent, *param._type)?;
			typenames[i] = strings::dup(strio::string(&strm));
			typenames[i] = strings::dup(memio::string(&strm)!);
			linelen += if (param.name == "") 1 else len(param.name);
			strio::reset(&strm);
			memio::reset(&strm);
		};
		switch (t.variadism) {
		case variadism::NONE => void;


@@ 295,7 295,7 @@ fn prototype_tty(
			linelen += 5;
		};
		linelen += type_tty(&strm, indent, *t.result)?;
		retname = strings::dup(strio::string(&strm));
		retname = strings::dup(memio::string(&strm)!);
		yield linelen;
	};


M cmd/haredoc/util.ha => cmd/haredoc/util.ha +4 -4
@@ 5,8 5,8 @@ use fmt;
use hare::ast;
use hare::module;
use io;
use memio;
use strings;
use strio;

// Forked from [[hare::unparse]].
fn newline(out: io::handle, indent: size) (size | io::error) = {


@@ 22,7 22,7 @@ fn multiline_comment(s: str) bool =
	strings::byteindex(s, '\n') as size != len(s) - 1;

fn trim_comment(s: str) str = {
	let trimmed = strio::dynamic();
	let trimmed = memio::dynamic();
	let tok = strings::tokenize(s, "\n");
	for (true) {
		const line = match (strings::next_token(&tok)) {


@@ 31,9 31,9 @@ fn trim_comment(s: str) str = {
		case let line: str =>
			yield line;
		};
		strio::concat(&trimmed, strings::trimprefix(line, " "), "\n")!;
		memio::concat(&trimmed, strings::trimprefix(line, " "), "\n")!;
	};
	return strings::dup(strio::string(&trimmed));
	return strings::dup(memio::string(&trimmed)!);
};

fn submodules(ctx: *context) ([]str | error) = {

M crypto/aes/+test/gcm.ha => crypto/aes/+test/gcm.ha +5 -5
@@ 1,8 1,8 @@
use bufio;
use bytes;
use crypto::cipher;
use errors;
use io;
use memio;

type gcmtestcase = struct {
	key: []u8,


@@ 605,7 605,7 @@ const gcmtestcases: []gcmtestcase = [
		} else {
			yield alloc([0...], len(t.cipher));
		};
		let resultbuf = bufio::fixed(result, io::mode::WRITE);
		let resultbuf = memio::fixed(result);

		let gstream = cipher::gcm();
		cipher::gcm_init(&gstream, &resultbuf, &b, t.iv, t.additional);


@@ 632,7 632,7 @@ const gcmtestcases: []gcmtestcase = [
		} else {
			yield alloc([0...], len(t.cipher));
		};
		let cipherbuf = bufio::fixed(t.cipher, io::mode::READ);
		let cipherbuf = memio::fixed(t.cipher);

		let gstream = cipher::gcm();
		cipher::gcm_init(&gstream, &cipherbuf, &b, t.iv, t.additional);


@@ 668,7 668,7 @@ const gcmtestcases: []gcmtestcase = [
			r[..] = t.plain[..];
			yield r;
		};
		let resultbuf = bufio::fixed(result, io::mode::WRITE);
		let resultbuf = memio::fixed(result);

		let gstream = cipher::gcm();
		// beware: did not close the stream for sake of simplicity


@@ 681,7 681,7 @@ const gcmtestcases: []gcmtestcase = [
		assert(bytes::equal(t.cipher, result));
		assert(bytes::equal(t.tag, tag));

		let resultbuf = bufio::fixed(result, io::mode::READ);
		let resultbuf = memio::fixed(result);
		let gstream = cipher::gcm();
		cipher::gcm_init(&gstream, &resultbuf, &b, t.iv, t.additional);
		io::readall(&gstream, result)!;

M crypto/aes/ctr+test.ha => crypto/aes/ctr+test.ha +12 -12
@@ 2,10 2,10 @@
// (c) 2022 Armin Preiml <apreiml@strohwolke.at>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use bytes;
use bufio;
use crypto::cipher;
use errors;
use io;
use memio;

@test fn ctr_zero_iv() void = {
	const key: [_]u8 = [


@@ 30,7 30,7 @@ use io;

	let result: [16]u8 = [0...];
	let buf: [CTR_BUFSIZE]u8 = [0...];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);

	let b = ct64();
	ct64_init(&b, key);


@@ 51,7 51,7 @@ use io;

	result = [0...];
	buf = [0...];
	let cipherbuf = bufio::fixed(cipher, io::mode::READ);
	let cipherbuf = memio::fixed(cipher);
	let ctr = cipher::ctr(&cipherbuf, &b, iv[..], buf[..]);
	const s = io::readall(&ctr, result)!;
	assert(s as size == len(plain));


@@ 82,7 82,7 @@ use io;
	];

	let result: [18]u8 = [0...];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);
	let buf: [CTR_BUFSIZE]u8 = [0...];

	let b = ct64();


@@ 133,7 133,7 @@ use io;
	];

	let result: [80]u8 = [0...];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);
	let buf: [CTR_BUFSIZE]u8 = [0...];

	let b = ct64();


@@ 148,7 148,7 @@ use io;
	let b = ct64();
	ct64_init(&b, key);

	let cipherbuf = bufio::fixed(cipher, io::mode::READ);
	let cipherbuf = memio::fixed(cipher);
	let ctr = cipher::ctr(&cipherbuf, &b, iv[..], buf[..]);
	const n = io::readall(&ctr, result)!;
	assert(n as size == len(plain));


@@ 194,7 194,7 @@ use io;
	];

	let result: [80]u8 = [0...];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);
	let buf: [CTR_BUFSIZE]u8 = [0...];

	let b = ct64();


@@ 210,7 210,7 @@ use io;
	assert(n == len(plain));
	assert(bytes::equal(cipher, result));

	let cipherbuf = bufio::fixed(cipher, io::mode::READ);
	let cipherbuf = memio::fixed(cipher);
	let ctr = cipher::ctr(&cipherbuf, &b, iv[..], buf[..]);
	const n = io::readall(&ctr, result)!;
	assert(n as size == len(plain));


@@ 245,7 245,7 @@ use io;
	defer cipher::finish(&b);

	let buf: [CTR_BUFSIZE]u8 = [0...];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);
	let ctr = cipher::ctr(&resultbuf, &b, iv[..], buf[..]);
	defer io::close(&ctr)!;



@@ 281,7 281,7 @@ use io;
	defer cipher::finish(&b);

	let buf: [64]u8 = [0...];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);

	let ctr = cipher::ctr(&resultbuf, &b, iv[..], buf[..]);
	defer io::close(&ctr)!;


@@ 307,7 307,7 @@ use io;

	let buf: [64]u8 = [0...];
	let result: [1]u8 = [0];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);

	let ctr = cipher::ctr(&resultbuf, &b, iv[..], buf[..]);
	defer io::close(&ctr)!;


@@ 397,7 397,7 @@ fn errwriter(out: io::handle, limit: size, err: io::error) err_stream = {
	];

	let result: [80]u8 = [0...];
	let resultbuf = bufio::fixed(result, io::mode::WRITE);
	let resultbuf = memio::fixed(result);
	let errw = errwriter(&resultbuf, 20, errors::again);
	let buf: [CTR_BUFSIZE]u8 = [0...];


M crypto/argon2/argon2.ha => crypto/argon2/argon2.ha +2 -2
@@ 1,7 1,6 @@
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021-2022 Armin Preiml <apreiml@strohwolke.at>
// (c) 2021-2022 Drew DeVault <sir@cmpwn.com>
use bufio;
use bytes;
use crypto::blake2b;
use crypto::math;


@@ 9,6 8,7 @@ use endian;
use errors::{nomem};
use hash;
use io;
use memio;
use types;

// Latest version of argon2 supported by this implementation (1.3).


@@ 344,7 344,7 @@ fn varhash(dest: []u8, block: []u8) void = {
	const r = divceil(len(dest): u32, 32) - 2;
	let v: [64]u8 = [0...];

	let destbuf = bufio::fixed(dest, io::mode::WRITE);
	let destbuf = memio::fixed(dest);

	let h = blake2b::blake2b([], 64);
	hash_leputu32(&h, len(dest): u32);

M crypto/authenc.ha => crypto/authenc.ha +4 -4
@@ 2,11 2,11 @@
// (c) 2023 Armin Preiml <apreiml@strohwolke.at>
// (c) 2023 Drew DeVault <sir@cmpwn.com>
use bytes;
use bufio;
use crypto::chachapoly;
use crypto::math;
use errors;
use io;
use memio;

// A secret session key.
export type sessionkey = [32]u8;


@@ 79,12 79,12 @@ export fn encrypt(
	let s = chachapoly::chachapoly();
	defer io::close(&s)!;

	let h = bufio::fixed(plaintext, io::mode::WRITE);
	let h = memio::fixed(plaintext);
	chachapoly::xinit(&s, &h, key, nonce, additional...);
	io::writeall(&s, plaintext)!;
	let m: mac = [0...];
	chachapoly::seal(&s, m);
	return (m, *nonce, bufio::buffer(&h));
	return (m, *nonce, memio::buffer(&h));
};

// Authenticates and decrypts a message encrypted with [[encrypt]]. If the


@@ 111,7 111,7 @@ export fn decrypt(
	defer io::close(&s)!;

	let ciphertext = box.2;
	let h = bufio::fixed(ciphertext, io::mode::READ);
	let h = memio::fixed(ciphertext);
	chachapoly::xinit(&s, &h, key, box.1, additional...);

	let plaintext = ciphertext;

M crypto/bcrypt/base64.ha => crypto/bcrypt/base64.ha +3 -3
@@ 4,10 4,10 @@
//
// bcrypt uses a crappy variant of base64 with its own special alphabet and no
// padding. This file glues encoding::base64 to the bcrypt semantics.
use bufio;
use encoding::base64;
use errors;
use io;
use memio;

const alpha: str = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const b64encoding: base64::encoding = base64::encoding { ... };


@@ 19,9 19,9 @@ const b64encoding: base64::encoding = base64::encoding { ... };
// Encodes a slice in the bcrypt base64 style, returning a new slice. The caller
// must free the return value.
fn b64_encode(src: []u8) []u8 = {
	let sink = bufio::dynamic(io::mode::WRITE);
	let sink = memio::dynamic();
	base64::encode(&sink, &b64encoding, src)!;
	let buf = bufio::buffer(&sink);
	let buf = memio::buffer(&sink);
	let i = len(buf);
	for (i > 0 && buf[i - 1] == '='; i -= 1) void;
	return buf[..i];

M crypto/bcrypt/bcrypt.ha => crypto/bcrypt/bcrypt.ha +3 -3
@@ 6,7 6,6 @@
// ported from Go.
//
// TODO: Move me into the extlib (hare-x-crypto?)
use bufio;
use bytes;
use crypto::blowfish;
use crypto::cipher;


@@ 15,6 14,7 @@ use crypto;
use errors;
use fmt;
use io;
use memio;
use strconv;
use strings;



@@ 78,7 78,7 @@ export fn compare(hash: []u8, password: []u8) (bool | errors::invalid) = {
};

fn mkhash(h: *hash) []u8 = {
	let buf = bufio::dynamic(io::mode::WRITE);
	let buf = memio::dynamic();
	fmt::fprintf(&buf, "${}$", h.major: u32: rune)!;
	if (h.minor != 0) {
		fmt::fprintf(&buf, "{}", h.minor: u32: rune)!;


@@ 86,7 86,7 @@ fn mkhash(h: *hash) []u8 = {
	fmt::fprintf(&buf, "${:02}$", h.cost)!;
	io::write(&buf, h.salt)!;
	io::write(&buf, h.hash)!;
	return bufio::buffer(&buf);
	return memio::buffer(&buf);
};

fn hash_password(

M crypto/blake2b/+test.ha => crypto/blake2b/+test.ha +6 -6
@@ 8,8 8,8 @@ use encoding::hex;
use fmt;
use hash;
use io;
use memio;
use strings;
use strio;

@test fn blake2b() void = {
	for (let i = 0z; i < len(vectors); i += 1) {


@@ 28,11 28,11 @@ use strio;
			append(sum, 0);
		};
		hash::sum(&blake, sum);
		let out = strio::dynamic();
		let out = memio::dynamic();
		defer io::close(&out)!;
		let enc = hex::newencoder(&out);
		io::write(&enc, sum)!;
		assert(strio::string(&out) == vectors[i].out);
		assert(memio::string(&out)! == vectors[i].out);
	};

	const vectors = [


@@ 56,16 56,16 @@ use strio;
		assert(len(sum) >= hash::sz(&blake));
		hash::sum(&blake, sum);

		let hex = strio::dynamic();
		let hex = memio::dynamic();
		defer io::close(&hex)!;

		for (let j = 0z; j < len(sum); j += 1) {
			fmt::fprintf(&hex, "{:02x}", sum[j])!;
		};

		if (strio::string(&hex) != vector.1) {
		if (memio::string(&hex)! != vector.1) {
			fmt::errorfln("Vector {}: {} != {}",
				i, strio::string(&hex), vector.1)!;
				i, memio::string(&hex)!, vector.1)!;
			abort();
		};
	};

M crypto/chacha/+test.ha => crypto/chacha/+test.ha +5 -5
@@ 1,9 1,9 @@
// License: MPL-2.0
// (c) 2022 Armin Preiml <apreiml@strohwolke.at>
use bufio;
use bytes;
use crypto::cipher;
use io;
use memio;

// test vector taken from rfc8439
@test fn chacha20() void = {


@@ 48,7 48,7 @@ use io;

	let result: [114]u8 = [0...];

	let cipherbuf = bufio::fixed(result, io::mode::WRITE);
	let cipherbuf = memio::fixed(result);

	let c = chacha20();
	defer io::close(&c)!;


@@ 61,7 61,7 @@ use io;
	assert(bytes::equal(cipher, result));

	result = [0...];
	cipherbuf = bufio::fixed(result, io::mode::WRITE);
	cipherbuf = memio::fixed(result);

	chacha20_init(&c, &cipherbuf, key, nonce);
	setctr(&c, 1);


@@ 155,7 155,7 @@ const xcipher: [_]u8 = [

@test fn xchacha20() void = {
	let result: [304]u8 = [0...];
	let cipherbuf = bufio::fixed(result, io::mode::WRITE);
	let cipherbuf = memio::fixed(result);

	let c = chacha20();
	defer io::close(&c)!;


@@ 169,7 169,7 @@ const xcipher: [_]u8 = [

@test fn skipblocks() void = {
	let result: [18]u8 = [0...];
	let cipherbuf = bufio::fixed(result, io::mode::WRITE);
	let cipherbuf = memio::fixed(result);

	let c = chacha20();
	defer io::close(&c)!;

M crypto/chachapoly/chachapoly.ha => crypto/chachapoly/chachapoly.ha +2 -2
@@ 1,6 1,5 @@
// License: MPL-2.0
// (c) 2023 Armin Preiml <apreiml@strohwolke.at>
use bufio;
use bytes;
use crypto::chacha;
use crypto::mac;


@@ 9,6 8,7 @@ use crypto::poly1305;
use endian;
use errors;
use io;
use memio;
use types;

// Nonce size as required by [[init]].


@@ 89,7 89,7 @@ fn geninit(
	let otk: poly1305::key = [0...];
	defer bytes::zero(otk);

	let otkbuf = bufio::fixed(otk, io::mode::WRITE);
	let otkbuf = memio::fixed(otk);
	finit(&s.c, &otkbuf, key, nonce);
	io::writeall(&s.c, otk[..])!;


M crypto/chachapoly/encryption+test.ha => crypto/chachapoly/encryption+test.ha +11 -11
@@ 1,8 1,8 @@
// License: MPL-2.0
// (c) 2023 Armin Preiml <apreiml@strohwolke.at>
use bufio;
use bytes;
use io;
use memio;

@test fn encrypt() void = {
	let plain: [_]u8 = [


@@ 57,7 57,7 @@ use io;
		0x2e, 0xcb, 0xd0, 0x60, 0x06, 0x91,
	];

	let out = bufio::dynamic(io::mode::RDWR);
	let out = memio::dynamic();
	defer io::close(&out)!;

	let s = chachapoly();


@@ 67,13 67,13 @@ use io;
	let outtag: [TAGSZ]u8 = [0...];
	seal(&s, outtag);

	let outbuf = bufio::buffer(&out);
	let outbuf = memio::buffer(&out);
	assert(bytes::equal(outbuf, cipher));
	assert(bytes::equal(outtag, tag));

	let out = bufio::dynamic(io::mode::RDWR);
	let out = memio::dynamic();
	defer io::close(&out)!;
	let in = bufio::fixed(cipher, io::mode::READ);
	let in = memio::fixed(cipher);

	let s = chachapoly();
	init(&s, &in, key, nonce, ad);


@@ 81,7 81,7 @@ use io;

	verify(&s, tag)!;

	let outbuf = bufio::buffer(&out);
	let outbuf = memio::buffer(&out);
	assert(bytes::equal(outbuf, plain));

	io::close(&s)!;


@@ 150,7 150,7 @@ const rfcsample: sample = sample {
@test fn xencrypt() void = {
	let tc = rfcsample;

	let out = bufio::dynamic(io::mode::RDWR);
	let out = memio::dynamic();
	defer io::close(&out)!;

	let s = chachapoly();


@@ 160,13 160,13 @@ const rfcsample: sample = sample {
	let outtag: [TAGSZ]u8 = [0...];
	seal(&s, outtag);

	let outbuf = bufio::buffer(&out);
	let outbuf = memio::buffer(&out);
	assert(bytes::equal(outbuf, tc.cipher));
	assert(bytes::equal(outtag, tc.mac));

	let out = bufio::dynamic(io::mode::RDWR);
	let out = memio::dynamic();
	defer io::close(&out)!;
	let in = bufio::fixed(tc.cipher, io::mode::READ);
	let in = memio::fixed(tc.cipher);

	let s = chachapoly();
	xinit(&s, &in, tc.key, tc.nonce, tc.additional);


@@ 174,7 174,7 @@ const rfcsample: sample = sample {

	verify(&s, tc.mac)!;

	let outbuf = bufio::buffer(&out);
	let outbuf = memio::buffer(&out);
	assert(bytes::equal(outbuf, tc.msg));

	io::close(&s)!;

M crypto/rsa/keys.ha => crypto/rsa/keys.ha +5 -5
@@ 1,12 1,12 @@
// License: MPL-2.0
// (c) 2022 Armin Preiml <apreiml@strohwolke.at>
use bufio;
use bytes;
use crypto::bigint;
use crypto::math::*;
use endian;
use errors;
use io;
use memio;
use types;

// The default bit size of RSA keys is 4096-bit. Used as base for buffer sizes.


@@ 78,7 78,7 @@ export fn pubkey_init(pubkey: []u8, x: pubparams) (size | error) = {
		return errors::invalid;
	};

	let w = bufio::fixed(pubkey, io::mode::WRITE);
	let w = memio::fixed(pubkey);

	let s = 0z;
	s += writeslice(&w, e)!;


@@ 180,7 180,7 @@ export fn privkey_init(privkey: []u8, x: privparams, n: []u8...) (size | error) 
	};

	let s = privkey_writehead(privkey, &x, n...)?;
	let w = bufio::fixed(privkey[s..], io::mode::WRITE);
	let w = memio::fixed(privkey[s..]);

	s += writeslice(&w, x.dp)!;
	s += writeslice(&w, x.dq)!;


@@ 233,7 233,7 @@ fn privkey_writehead(
		return errors::invalid;
	};

	let w = bufio::fixed(privkey, io::mode::WRITE);
	let w = memio::fixed(privkey);
	let lenbuf: [2]u8 = [0...];
	endian::beputu16(lenbuf, nbitlen: u16);
	return io::write(&w, lenbuf)!;


@@ 262,7 262,7 @@ export fn privkey_initd(
	s += privkey_dmod(privkey[s..], d, x.p);
	s += privkey_dmod(privkey[s..], d, x.q);

	let w = bufio::fixed(privkey[s..], io::mode::WRITE);
	let w = memio::fixed(privkey[s..]);
	s += writeslice(&w, x.iq)!;
	s += writeslice(&w, x.p)!;
	s += writeslice(&w, x.q)!;

M crypto/salsa/+test.ha => crypto/salsa/+test.ha +5 -5
@@ 1,10 1,10 @@
// License: MPL-2.0
// (c) 2021 Armin Preiml <apreiml@strohwolke.at>
use bufio;
use bytes;
use crypto::cipher;
use types;
use io;
use memio;
use types;

@test fn qr() void = {
	let s: [4]u32 = [0xe7e8c006, 0xc4f9417d, 0x6479b4b2, 0x68c67137];


@@ 57,7 57,7 @@ use io;
	];

	let result: [116]u8 = [0...];
	let cipherbuf = bufio::fixed(result, io::mode::WRITE);
	let cipherbuf = memio::fixed(result);

	let c = salsa20();
	defer io::close(&c)!;


@@ 110,7 110,7 @@ use io;
	];

	let result: [116]u8 = [0...];
	let cipherbuf = bufio::fixed(result, io::mode::WRITE);
	let cipherbuf = memio::fixed(result);

	let c = salsa20();
	defer io::close(&c)!;


@@ 165,7 165,7 @@ use io;


	let result: [116]u8 = [0...];
	let cipherbuf = bufio::fixed(result, io::mode::WRITE);
	let cipherbuf = memio::fixed(result);

	let c = salsa20();
	defer io::close(&c)!;

M encoding/base32/base32.ha => encoding/base32/base32.ha +13 -13
@@ 1,10 1,10 @@
// License: MPL-2.0
// (c) 2022 Ajay R <ar324@protonmail.com>
use ascii;
use bufio;
use bytes;
use errors;
use io;
use memio;
use os;
use strings;



@@ 166,11 166,11 @@ fn encode_closer(s: *io::stream) (void | io::error) = {
// Encodes a byte slice in base-32, using the given encoding, returning a slice
// of ASCII bytes. The caller must free the return value.
export fn encodeslice(enc: *encoding, in: []u8) []u8 = {
	let out = bufio::dynamic(io::mode::WRITE);
	let out = memio::dynamic();
	let encoder = newencoder(enc, &out);
	io::writeall(&encoder, in)!;
	io::close(&encoder)!;
	return bufio::buffer(&out);
	return memio::buffer(&out);
};

// Encodes a byte slice in base-32, using the given encoding, returning a


@@ 201,11 201,11 @@ export fn encodestr(enc: *encoding, in: []u8) str = {
		"CPNMUOJ1E8======",
	];
	for (let i = 0z; i <= len(in); i += 1) {
		let out = bufio::dynamic(io::mode::RDWR);
		let out = memio::dynamic();
		let enc = newencoder(&std_encoding, &out);
		io::writeall(&enc, in[..i]) as size;
		io::close(&enc)!;
		let outb = bufio::buffer(&out);
		let outb = memio::buffer(&out);
		assert(bytes::equal(outb, strings::toutf8(expect[i])));
		free(outb);
		// Testing encodestr should cover encodeslice too


@@ 213,11 213,11 @@ export fn encodestr(enc: *encoding, in: []u8) str = {
		defer free(s);
		assert(s == expect[i]);

		out = bufio::dynamic(io::mode::RDWR);
		out = memio::dynamic();
		enc = newencoder(&hex_encoding, &out);
		io::writeall(&enc, in[..i]) as size;
		io::close(&enc)!;
		let outb = bufio::buffer(&out);
		let outb = memio::buffer(&out);
		assert(bytes::equal(outb, strings::toutf8(expect_hex[i])));
		free(outb);
		let s = encodestr(&hex_encoding, in[..i]);


@@ 361,15 361,15 @@ export fn decodeslice(
	enc: *encoding,
	in: []u8,
) ([]u8 | errors::invalid) = {
	let in = bufio::fixed(in, io::mode::READ);
	let in = memio::fixed(in);
	let decoder = newdecoder(enc, &in);
	let out = bufio::dynamic(io::mode::WRITE);
	let out = memio::dynamic();
	match (io::copy(&out, &decoder)) {
	case io::error =>
		io::close(&out)!;
		return errors::invalid;
	case size =>
		return bufio::buffer(&out);
		return memio::buffer(&out);
	};
};



@@ 397,7 397,7 @@ export fn decodestr(enc: *encoding, in: str) ([]u8 | errors::invalid) = {
		("CPNMUOJ1E8======", "foobar", &hex_encoding),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		let in = bufio::fixed(strings::toutf8(cases[i].0), io::mode::READ);
		let in = memio::fixed(strings::toutf8(cases[i].0));
		let dec = newdecoder(cases[i].2, &in);
		let buf: [1]u8 = [0];
		let out: []u8 = [];


@@ 419,7 419,7 @@ export fn decodestr(enc: *encoding, in: str) ([]u8 | errors::invalid) = {
	};
	// Repeat of the above, but with a larger buffer
	for (let i = 0z; i < len(cases); i += 1) {
		let in = bufio::fixed(strings::toutf8(cases[i].0), io::mode::READ);
		let in = memio::fixed(strings::toutf8(cases[i].0));
		let dec = newdecoder(cases[i].2, &in);
		let buf: [1024]u8 = [0...];
		let out: []u8 = [];


@@ 454,7 454,7 @@ export fn decodestr(enc: *encoding, in: str) ([]u8 | errors::invalid) = {
		("CPNG====CPNG====", &std_encoding),
	];
	for (let i = 0z; i < len(invalid); i += 1) {
		let in = bufio::fixed(strings::toutf8(invalid[i].0), io::mode::READ);
		let in = memio::fixed(strings::toutf8(invalid[i].0));
		let dec = newdecoder(invalid[i].1, &in);
		let buf: [1]u8 = [0...];
		let valid = false;

M encoding/base64/base64.ha => encoding/base64/base64.ha +13 -13
@@ 6,10 6,10 @@
// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz>
// (c) 2023 Armin Preiml <apreiml@strohwolke.at>
use ascii;
use bufio;
use bytes;
use errors;
use io;
use memio;
use os;
use strings;



@@ 211,24 211,24 @@ fn clear(e: *encoder) void = {
	];
	const expected: str = `AAAAB3NzaC1yc2EA`;

	let buf = bufio::dynamic(io::mode::WRITE);
	let buf = memio::dynamic();
	let e = newencoder(&std_encoding, &buf);
	io::writeall(&e, raw[..4])!;
	io::writeall(&e, raw[4..11])!;
	io::writeall(&e, raw[11..])!;
	io::close(&e)!;

	assert(strings::fromutf8(bufio::buffer(&buf))! == expected);
	assert(memio::string(&buf)! == expected);
};

// Encodes a byte slice in base 64, using the given encoding, returning a slice
// of ASCII bytes. The caller must free the return value.
export fn encodeslice(enc: *encoding, in: []u8) []u8 = {
	let out = bufio::dynamic(io::mode::WRITE);
	let out = memio::dynamic();
	let encoder = newencoder(enc, &out);
	io::writeall(&encoder, in)!;
	io::close(&encoder)!;
	return bufio::buffer(&out);
	return memio::buffer(&out);
};

// Encodes base64 data using the given alphabet and writes it to a stream,


@@ 268,11 268,11 @@ export fn encodestr(enc: *encoding, in: []u8) str = {
		"Zm9vYmFy"
	];
	for (let i = 0z; i <= len(in); i += 1) {
		let out = bufio::dynamic(io::mode::WRITE);
		let out = memio::dynamic();
		let encoder = newencoder(&std_encoding, &out);
		io::writeall(&encoder, in[..i])!;
		io::close(&encoder)!;
		let encb = bufio::buffer(&out);
		let encb = memio::buffer(&out);
		defer free(encb);
		assert(bytes::equal(encb, strings::toutf8(expect[i])));



@@ 418,15 418,15 @@ export fn decodeslice(
	enc: *encoding,
	in: []u8,
) ([]u8 | errors::invalid) = {
	let in = bufio::fixed(in, io::mode::READ);
	let in = memio::fixed(in);
	let decoder = newdecoder(enc, &in);
	let out = bufio::dynamic(io::mode::WRITE);
	let out = memio::dynamic();
	match (io::copy(&out, &decoder)) {
	case io::error =>
		io::close(&out)!;
		return errors::invalid;
	case size =>
		return bufio::buffer(&out);
		return memio::buffer(&out);
	};
};



@@ 466,7 466,7 @@ export fn decode(
		("Zm9vYmFy", "foobar", &std_encoding),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		let in = bufio::fixed(strings::toutf8(cases[i].0), io::mode::READ);
		let in = memio::fixed(strings::toutf8(cases[i].0));
		let decoder = newdecoder(cases[i].2, &in);
		let buf: [1]u8 = [0];
		let decb: []u8 = [];


@@ 488,7 488,7 @@ export fn decode(
	};
	// Repeat of the above, but with a larger buffer
	for (let i = 0z; i < len(cases); i += 1) {
		let in = bufio::fixed(strings::toutf8(cases[i].0), io::mode::READ);
		let in = memio::fixed(strings::toutf8(cases[i].0));
		let decoder = newdecoder(cases[i].2, &in);
		let buf: [1024]u8 = [0...];
		let decb: []u8 = [];


@@ 519,7 519,7 @@ export fn decode(
		("Zm8=Zm8=", &std_encoding),
	];
	for (let i = 0z; i < len(invalid); i += 1) {
		let in = bufio::fixed(strings::toutf8(invalid[i].0), io::mode::READ);
		let in = memio::fixed(strings::toutf8(invalid[i].0));
		let decoder = newdecoder(invalid[i].1, &in);
		let buf: [1]u8 = [0...];
		let valid = false;

M encoding/hex/hex.ha => encoding/hex/hex.ha +10 -11
@@ 4,15 4,14 @@
// (c) 2021 Ember Sawady <ecs@d2evs.net>
// (c) 2022 Sebastian <sebastian@sebsite.pw>
use ascii;
use bufio;
use bytes;
use errors;
use fmt;
use io;
use memio;
use os;
use strconv;
use strings;
use strio;

export type encoder = struct {
	stream: io::stream,


@@ 70,10 69,10 @@ fn encode_writer(s: *io::stream, in: const []u8) (size | io::error) = {
// Encodes a byte slice as a hexadecimal string and returns it. The caller must
// free the return value.
export fn encodestr(in: []u8) str = {
	const out = strio::dynamic();
	const out = memio::dynamic();
	const enc = newencoder(&out);
	io::writeall(&enc, in)!;
	return strio::string(&out);
	return memio::string(&out)!;
};

@test fn encodestr() void = {


@@ 93,11 92,11 @@ export fn encode(out: io::handle, in: []u8) (size | io::error) = {
@test fn encode() void = {
	const in: [_]u8 = [0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xF0, 0x0D];

	let out = strio::dynamic();
	let out = memio::dynamic();
	defer io::close(&out)!;

	encode(&out, in)!;
	assert(strio::string(&out) == "cafebabedeadf00d");
	assert(memio::string(&out)! == "cafebabedeadf00d");
};

export type decoder = struct {


@@ 172,12 171,12 @@ fn decode_reader(s: *io::stream, out: []u8) (size | io::EOF | io::error) = {
// the return value.
export fn decodestr(s: str) ([]u8 | io::error) = {
	let s = strings::toutf8(s);
	const in = bufio::fixed(s, io::mode::READ);
	const in = memio::fixed(s);
	const decoder = newdecoder(&in);
	const out = bufio::dynamic(io::mode::WRITE);
	const out = memio::dynamic();
	match(io::copy(&out, &decoder)) {
	case size =>
	     return bufio::buffer(&out);
	     return memio::buffer(&out);
	case let err: io::error =>
	     return err;
	};


@@ 234,11 233,11 @@ export fn dump(out: io::handle, data: []u8) (void | io::error) = {
		0xDE, 0xAD, 0xF0, 0x0D
	];

	let sink = strio::dynamic();
	let sink = memio::dynamic();
	defer io::close(&sink)!;
	dump(&sink, in) as void;

	let s = strio::string(&sink);
	let s = memio::string(&sink)!;
	assert(s ==
		"00000000  7f 45 4c 46 02 01 01 00  ca fe ba be de ad f0 0d  |.ELF............|\n"
		"00000010  ce fe ba be de ad f0 0d                           |........|\n");

M encoding/pem/+test.ha => encoding/pem/+test.ha +10 -11
@@ 1,18 1,17 @@
// License: MPL-2.0
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use bufio;
use bytes;
use io;
use fmt;
use memio;
use strings;
use strio;


@test fn read() void = {
	const testcert_str = fmt::asprintf(
		"garbage\ngarbage\ngarbage\n{}garbage\n", cert_str);
	defer free(testcert_str);
	const in = bufio::fixed(strings::toutf8(testcert_str), io::mode::READ);
	const in = memio::fixed(strings::toutf8(testcert_str));
	const dec = newdecoder(&in);
	defer finish(&dec);



@@ 30,7 29,7 @@ use strio;
@test fn read_many() void = {
	const testmany = fmt::asprintf("{}{}", cert_str, privkey_str);
	defer free(testmany);
	const in = bufio::fixed(strings::toutf8(testmany), io::mode::READ);
	const in = memio::fixed(strings::toutf8(testmany));
	const dec = newdecoder(&in);
	defer finish(&dec);



@@ 49,28 48,28 @@ use strio;
};

@test fn write() void = {
	let out = strio::dynamic();
	let out = memio::dynamic();
	const stream = newencoder("CERTIFICATE", &out)!;
	io::writeall(&stream, testcert_bin)!;
	io::close(&stream)!;
	assert(strio::string(&out) == cert_str);
	assert(memio::string(&out)! == cert_str);
	io::close(&out)!;

	let out = strio::dynamic();
	let out = memio::dynamic();
	const stream = newencoder("PRIVATE KEY", &out)!;
	io::writeall(&stream, testprivkey_bin)!;
	io::close(&stream)!;
	assert(strio::string(&out) == privkey_str);
	assert(memio::string(&out)! == privkey_str);
	io::close(&out)!;

	// test short writes
	let out = strio::dynamic();
	let out = memio::dynamic();
	const stream = newencoder("CERTIFICATE", &out)!;
	for (let i = 0z; i < len(testcert_bin); i += 1) {
		io::write(&stream, [testcert_bin[i]])!;
	};
	io::close(&stream)!;
	assert(strio::string(&out) == cert_str);
	assert(memio::string(&out)! == cert_str);
	io::close(&out)!;
};



@@ 168,7 167,7 @@ const testprivkey_bin: [_]u8 = [
	const test_str = fmt::asprintf(
		"garbage\r\ngarbage\r\ngarbage\r\n{}garbage\r\n", testcrlf_str);
	defer free(test_str);
	const in = bufio::fixed(strings::toutf8(test_str), io::mode::READ);
	const in = memio::fixed(strings::toutf8(test_str));
	const dec = newdecoder(&in);
	defer finish(&dec);


M encoding/pem/pem.ha => encoding/pem/pem.ha +10 -10
@@ 6,9 6,9 @@ use encoding::base64;
use errors;
use fmt;
use io;
use memio;
use os;
use strings;
use strio;


const begin: str = "-----BEGIN ";


@@ 16,19 16,19 @@ const end: str = "-----END ";
const suffix: str = "-----";

export type decoder = struct {
	in: bufio::bufstream,
	label: strio::stream,
	in: bufio::stream,
	label: memio::stream,
	buf: []u8,
};

export type b64stream = struct {
	stream: io::stream,
	in: *bufio::bufstream,
	in: *bufio::stream,
};

export type pemdecoder = struct {
	stream: io::stream,
	in: *bufio::bufstream,
	in: *bufio::stream,
	b64_in: b64stream,
	b64: base64::decoder,
	// XXX: kind of dumb but it saves us some memory management problems


@@ 50,9 50,9 @@ const b64stream_r_vt: io::vtable = io::vtable {
export fn newdecoder(in: io::handle) decoder = {
	let buf: []u8 = alloc([0...], os::BUFSIZ);
	return decoder {
		in = bufio::buffered(in, buf, []),
		in = bufio::init(in, buf, []),
		buf = buf,
		label = strio::dynamic(),
		label = memio::dynamic(),
	};
};



@@ 104,12 104,12 @@ export fn next(dec: *decoder) ((str, pemdecoder) | io::EOF | io::error) = {
			continue;
		};

		strio::reset(&dec.label);
		memio::reset(&dec.label);
		const label = strings::sub(line,
			len(begin), len(line) - len(suffix));
		strio::concat(&dec.label, label)!;
		memio::concat(&dec.label, label)!;

		return (strio::string(&dec.label), pemdecoder {
		return (memio::string(&dec.label)!, pemdecoder {
			stream = &pemdecoder_vt,
			in = &dec.in,
			b64_in = b64stream {

M fmt/fmt.ha => fmt/fmt.ha +7 -7
@@ 6,10 6,10 @@
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use ascii;
use bufio;
use encoding::utf8;
use io;
use math;
use memio;
use os;
use strconv;
use strings;


@@ 44,16 44,16 @@ export fn errorfln(fmt: str, args: field...) (size | io::error) =
// Formats text for printing and writes it into a heap-allocated string. The
// caller must free the return value.
export fn asprintf(fmt: str, args: field...) str = {
	let buf = bufio::dynamic(io::mode::WRITE);
	let buf = memio::dynamic();
	assert(fprintf(&buf, fmt, args...) is size);
	return strings::fromutf8_unsafe(bufio::buffer(&buf));
	return strings::fromutf8_unsafe(memio::buffer(&buf));
};

// Formats text for printing and writes it into a caller supplied buffer. The
// returned string is borrowed from this buffer. Aborts if the buffer isn't
// large enough to hold the formatted text.
export fn bsprintf(buf: []u8, fmt: str, args: field...) str = {
	let sink = bufio::fixed(buf, io::mode::WRITE);
	let sink = memio::fixed(buf);
	let l = fprintf(&sink, fmt, args...)!;
	return strings::fromutf8_unsafe(buf[..l]);
};


@@ 107,9 107,9 @@ export fn errorln(args: formattable...) (size | io::error) =
// them into a heap-allocated string separated by spaces. The caller must free
// the return value.
export fn asprint(args: formattable...) str = {
	let buf = bufio::dynamic(io::mode::WRITE);
	let buf = memio::dynamic();
	assert(fprint(&buf, args...) is size);
	return strings::fromutf8_unsafe(bufio::buffer(&buf));
	return strings::fromutf8_unsafe(memio::buffer(&buf));
};

// Formats values for printing using the default format modifiers and writes


@@ 117,7 117,7 @@ export fn asprint(args: formattable...) str = {
// is borrowed from this buffer. Aborts if the buffer isn't large enough to hold
// the formatted text.
export fn bsprint(buf: []u8, args: formattable...) str = {
	let sink = bufio::fixed(buf, io::mode::WRITE);
	let sink = memio::fixed(buf);
	let l = fprint(&sink, args...)!;
	return strings::fromutf8_unsafe(buf[..l]);
};

M format/ini/+test.ha => format/ini/+test.ha +7 -7
@@ 1,18 1,18 @@
// License: MPL-2.0
// (c) 2021-2022 Alexey Yerin <yyp@disroot.org>
use bufio;
use io;
use memio;
use strings;

@test fn simple() void = {
	const buf = bufio::fixed(strings::toutf8(
	const buf = memio::fixed(strings::toutf8(
"# This is a comment
[sourcehut.org]
name=Sourcehut
description=The hacker's forge
[harelang.org]
name=Hare
description=The Hare programming language"), io::mode::READ);
description=The Hare programming language"));
	const sc = scan(&buf);
	defer finish(&sc);



@@ 28,13 28,13 @@ description=The Hare programming language"), io::mode::READ);

@test fn extended() void = {
	// TODO: expand?
	const buf = bufio::fixed(strings::toutf8(
	const buf = memio::fixed(strings::toutf8(
"# Equal sign in the value
exec=env VARIABLE=value binary

# Unicode
trademark=™
"), io::mode::READ);
"));
	const sc = scan(&buf);
	defer finish(&sc);



@@ 46,14 46,14 @@ trademark=™

@test fn invalid() void = {
	// Missing equal sign
	const buf = bufio::fixed(strings::toutf8("novalue\n"), io::mode::READ);
	const buf = memio::fixed(strings::toutf8("novalue\n"));
	const sc = scan(&buf);
	defer finish(&sc);

	assert(next(&sc) as error is syntaxerr); // TODO: test line numbering?

	// Unterminated section header
	const buf = bufio::fixed(strings::toutf8("[dangling\n"), io::mode::READ);
	const buf = memio::fixed(strings::toutf8("[dangling\n"));
	const sc = scan(&buf);
	defer finish(&sc);


M format/tar/reader.ha => format/tar/reader.ha +9 -10
@@ 1,12 1,11 @@
// License: MPL-2.0
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use bufio;
use bytes;
use endian;
use errors;
use io;
use memio;
use strconv;
use strio;
use types::c;

export type reader = struct {


@@ 58,7 57,7 @@ export fn next(rd: *reader) (entry | error | io::EOF) = {
		return io::EOF;
	};

	const reader = bufio::fixed(buf, io::mode::READ);
	const reader = memio::fixed(buf);
	const name = readstr(&reader, 100);
	const mode = readoct(&reader, 8)?;
	const uid = readoct(&reader, 8)?;


@@ 100,9 99,9 @@ export fn next(rd: *reader) (entry | error | io::EOF) = {
	ent.devmajor = readoct(&reader, 8)?;
	ent.devminor = readoct(&reader, 8)?;
	const prefix = readstr(&reader, 155);
	let writer = strio::fixed(rd.name);
	strio::join(&writer, prefix, name)!;
	ent.name = strio::string(&writer);
	let writer = memio::fixed(rd.name);
	memio::join(&writer, prefix, name)!;
	ent.name = memio::string(&writer)!;
	return ent;
};



@@ 199,8 198,8 @@ fn file_seek(
	return new;
};

fn readstr(rd: *bufio::memstream, ln: size) str = {
	const buf = match (bufio::borrowedread(rd, ln)) {
fn readstr(rd: *memio::stream, ln: size) str = {
	const buf = match (memio::borrowedread(rd, ln)) {
	case let buf: []u8 =>
		assert(len(buf) == ln);
		yield buf;


@@ 210,7 209,7 @@ fn readstr(rd: *bufio::memstream, ln: size) str = {
	return c::tostr(buf: *[*]u8: *const c::char)!;
};

fn readoct(rd: *bufio::memstream, ln: size) (uint | invalid) = {
fn readoct(rd: *memio::stream, ln: size) (uint | invalid) = {
	const string = readstr(rd, ln);
	match (strconv::stoub(string, strconv::base::OCT)) {
	case let u: uint =>


@@ 220,7 219,7 @@ fn readoct(rd: *bufio::memstream, ln: size) (uint | invalid) = {
	};
};

fn readsize(rd: *bufio::memstream, ln: size) (size | invalid) = {
fn readsize(rd: *memio::stream, ln: size) (size | invalid) = {
	const string = readstr(rd, ln);
	match (strconv::stozb(string, strconv::base::OCT)) {
	case let z: size =>

M glob/glob.ha => glob/glob.ha +37 -37
@@ 3,10 3,10 @@
use fnmatch;
use fs;
use io;
use memio;
use os;
use sort;
use strings;
use strio;

// Flags used to control the behavior of [[next]].
export type flag = enum uint {


@@ 33,15 33,15 @@ export type generator = struct {
};

export type strstack = struct {
	bufv: []strio::stream,
	bufv: []memio::stream,
	bufc: size,
};

export type pattern = struct {
	// TODO: look into working with a couple of string iterators instead
	dir: strio::stream,
	pat: strio::stream,
	rem: strio::stream,
	dir: memio::stream,
	pat: memio::stream,
	rem: memio::stream,
};

// Information about an unsuccessful search.


@@ 56,7 56,7 @@ export type failure = !struct {
// freed using [[finish]].
export fn glob(pattern: str, flags: flag...) generator = {
	let ss = strstack_init();
	strio::concat(strstack_push(&ss), pattern)!;
	memio::concat(strstack_push(&ss), pattern)!;
	let bs = flag::NONE;
	for (let i = 0z; i < len(flags); i += 1) {
		bs |= flags[i];


@@ 81,9 81,9 @@ export fn finish(gen: *generator) void = {
// [[next]] can be repeatedly called until void is returned.
export fn next(gen: *generator) (str | void | failure) = {
	const init = strstack_size(&gen.pats) == 1
		&& len(strio::string(&gen.tmpp.dir)) == 0
		&& len(strio::string(&gen.tmpp.pat)) == 0
		&& len(strio::string(&gen.tmpp.rem)) == 0;
		&& len(memio::string(&gen.tmpp.dir)!) == 0
		&& len(memio::string(&gen.tmpp.pat)!) == 0
		&& len(memio::string(&gen.tmpp.rem)!) == 0;
	match (next_match(os::cwd, gen)) {
	case let s: str =>
		return s;


@@ 92,7 92,7 @@ export fn next(gen: *generator) (str | void | failure) = {
	case void => void;
	};
	if (init && gen.flgs & flag::NOCHECK != 0) {
		return strio::string(&gen.pats.bufv[0]);
		return memio::string(&gen.pats.bufv[0])!;
	};
};



@@ 145,10 145,10 @@ fn next_match(fs: *fs::fs, gen: *generator) (str | void | failure) = {

		let b = strstack_push(&gen.pats);
		if (len(rem) > 0) {
			strio::concat(b, dir, de.name, "/", rem)!;
			memio::concat(b, dir, de.name, "/", rem)!;
			continue;
		};
		strio::concat(b, dir, de.name)!;
		memio::concat(b, dir, de.name)!;
		if (patm || gen.flgs & flag::MARK != 0) {
			let m = fs::isdir(de.ftype);
			// POSIX does not specify the behavior when a pathname


@@ 156,7 156,7 @@ fn next_match(fs: *fs::fs, gen: *generator) (str | void | failure) = {
			// directory. But in major implementation a slash
			// character is appended in this case.
			if (fs::islink(de.ftype)) {
				match (fs::realpath(fs, strio::string(b))) {
				match (fs::realpath(fs, memio::string(b)!)) {
				case let r: str =>
					match (fs::stat(fs, r)) {
					case let s: fs::filestat =>


@@ 167,7 167,7 @@ fn next_match(fs: *fs::fs, gen: *generator) (str | void | failure) = {
				};
			};
			if (m) {
				strio::concat(b, "/")!;
				memio::concat(b, "/")!;
			} else if (patm) {
				strstack_pop(&gen.pats);
				continue;


@@ 183,9 183,9 @@ fn next_match(fs: *fs::fs, gen: *generator) (str | void | failure) = {
};

fn pattern_init() pattern = pattern {
	dir = strio::dynamic(),
	pat = strio::dynamic(),
	rem = strio::dynamic(),
	dir = memio::dynamic(),
	pat = memio::dynamic(),
	rem = memio::dynamic(),
};

fn pattern_free(p: *pattern) void = {


@@ 195,16 195,16 @@ fn pattern_free(p: *pattern) void = {
};

fn pattern_reset(p: *pattern) void = {
	strio::reset(&p.dir);
	strio::reset(&p.pat);
	strio::reset(&p.rem);
	memio::reset(&p.dir);
	memio::reset(&p.pat);
	memio::reset(&p.rem);
};

fn pattern_dir(p: *pattern) str = strio::string(&p.dir);
fn pattern_dir(p: *pattern) str = memio::string(&p.dir)!;

fn pattern_pat(p: *pattern) str = strio::string(&p.pat);
fn pattern_pat(p: *pattern) str = memio::string(&p.pat)!;

fn pattern_rem(p: *pattern) str = strio::string(&p.rem);
fn pattern_rem(p: *pattern) str = memio::string(&p.rem)!;

fn pattern_parse(p: *pattern, pstr: str, noesc: bool) void = {
	pattern_reset(p);


@@ 239,10 239,10 @@ fn pattern_parse(p: *pattern, pstr: str, noesc: bool) void = {
		case => void;
		};

		strio::appendrune(&p.pat, r)!;
		memio::appendrune(&p.pat, r)!;
		if (r == '/') {
			strio::concat(&p.dir, strio::string(&p.pat))!;
			strio::reset(&p.pat);
			memio::concat(&p.dir, memio::string(&p.pat)!)!;
			memio::reset(&p.pat);
			itpat = itdir;
		};
		esc = false;


@@ 250,7 250,7 @@ fn pattern_parse(p: *pattern, pstr: str, noesc: bool) void = {
	
	// p.pat is the first path component which contains special
	// characters.
	strio::reset(&p.pat);
	memio::reset(&p.pat);
	for (let esc = false; true) {
		const r = match (strings::next(&itpat)) {
		case void =>


@@ 265,16 265,16 @@ fn pattern_parse(p: *pattern, pstr: str, noesc: bool) void = {
		};

		if (esc && r != '/') {
			strio::appendrune(&p.pat, '\\')!;
			memio::appendrune(&p.pat, '\\')!;
		};
		strio::appendrune(&p.pat, r)!;
		memio::appendrune(&p.pat, r)!;
		if (r == '/') {
			break;
		};
		esc = false;
	};

	strio::concat(&p.rem, strings::iterstr(&itpat))!;
	memio::concat(&p.rem, strings::iterstr(&itpat))!;
};

fn strstack_init() strstack = strstack {


@@ 291,12 291,12 @@ fn strstack_free(ss: *strstack) void = {

fn strstack_size(ss: *strstack) size = ss.bufc;

fn strstack_push(ss: *strstack) *strio::stream = {
fn strstack_push(ss: *strstack) *memio::stream = {
	if (ss.bufc == len(ss.bufv)) {
		append(ss.bufv, strio::dynamic());
		append(ss.bufv, memio::dynamic());
	};
	let b = &ss.bufv[ss.bufc];
	strio::reset(b);
	memio::reset(b);
	ss.bufc += 1;
	return b;
};


@@ 306,7 306,7 @@ fn strstack_pop(ss: *strstack) (str | void) = {
		return;
	};
	ss.bufc -= 1;
	return strio::string(&ss.bufv[ss.bufc]);
	return memio::string(&ss.bufv[ss.bufc])!;
};

fn strstack_sort(ss: *strstack, pos: size) void = {


@@ 314,11 314,11 @@ fn strstack_sort(ss: *strstack, pos: size) void = {
		return;
	};
	let s = ss.bufv[pos..ss.bufc];
	sort::sort(s, size(strio::stream), &bufcmp);
	sort::sort(s, size(memio::stream), &bufcmp);
};

fn bufcmp(a: const *void, b: const *void) int =
	strings::compare(
		strio::string(b: *strio::stream),
		strio::string(a: *strio::stream),
		memio::string(b: *memio::stream)!,
		memio::string(a: *memio::stream)!,
	);

M hare/lex/+test.ha => hare/lex/+test.ha +10 -10
@@ 5,14 5,14 @@
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
// (c) 2021 Sudipto Mallick <smlckz@disroot.org>
use bufio;
use fmt;
use io;
use io::{mode};
use memio;
use strings;

@test fn unget() void = {
	let buf = bufio::fixed(strings::toutf8("z"), mode::READ);
	let buf = memio::fixed(strings::toutf8("z"));
	let lexer = init(&buf, "<test>");
	unget(&lexer, ('x', location { path = "<test>", line = 1, col = 2 }));
	unget(&lexer, ('y', location { path = "<test>", line = 1, col = 3 }));


@@ 60,7 60,7 @@ fn vassert(expected: value, actual: value) void = {
};

fn lextest(in: str, expected: []token) void = {
	let buf = bufio::fixed(strings::toutf8(in), mode::READ);
	let buf = memio::fixed(strings::toutf8(in));
	let lexer = init(&buf, "<test>");
	for (let i = 0z; i < len(expected); i += 1) {
		let etok = expected[i];


@@ 188,7 188,7 @@ fn loc(line: uint, col: uint) location = location {
@test fn keywords() void = {
	let keywords = bmap[..ltok::LAST_KEYWORD+1];
	for (let i = 0z; i < len(keywords); i += 1) {
		let buf = bufio::fixed(strings::toutf8(keywords[i]), mode::READ);
		let buf = memio::fixed(strings::toutf8(keywords[i]));
		let lexer = init(&buf, "<test>");
		let tok = lex(&lexer) as token;
		assert(tok.0 == i: ltok);


@@ 205,7 205,7 @@ fn loc(line: uint, col: uint) location = location {
	lextest(in, expected);

	let in = "// foo\n// bar\nhello world// baz\n\n// bad\ntest";
	let buf = bufio::fixed(strings::toutf8(in), mode::READ);
	let buf = memio::fixed(strings::toutf8(in));
	let lexer = init(&buf, "<input>", flag::COMMENTS);
	assert(lex(&lexer) is token);
	assert(comment(&lexer) == " foo\n bar\n");


@@ 307,7 307,7 @@ fn loc(line: uint, col: uint) location = location {
	// empty string
	const in = ['1': u8, 0x80];

	let buf = bufio::fixed(in, mode::READ);
	let buf = memio::fixed(in);
	let lexer = init(&buf, "<test>");

	const s = lex(&lexer) as error as syntax;


@@ 317,7 317,7 @@ fn loc(line: uint, col: uint) location = location {
	// a crash in nextw
	const in = [0x80: u8];

	let buf = bufio::fixed(in, mode::READ);
	let buf = memio::fixed(in);
	let lexer = init(&buf, "<test>");

	const s = lex(&lexer) as error as syntax;


@@ 327,7 327,7 @@ fn loc(line: uint, col: uint) location = location {
	// crash
	const in = ['"': u8, '\\': u8, '^': u8, '"': u8];

	let buf = bufio::fixed(in, mode::READ);
	let buf = memio::fixed(in);
	let lexer = init(&buf, "<test>");

	const s = lex(&lexer) as error as syntax;


@@ 336,7 336,7 @@ fn loc(line: uint, col: uint) location = location {
	// Regression: <X>e followed by another token used to cause a crash
	const in = ['0': u8, 'e': u8, ')': u8];

	let buf = bufio::fixed(in, mode::READ);
	let buf = memio::fixed(in);
	let lexer = init(&buf, "<test>");

	const s = lex(&lexer) as error as syntax;


@@ 358,7 358,7 @@ type op = enum {

@test fn loc() void = {
	const src = "h 	ello: my	name is Inigo Montoya";
	let buf = bufio::fixed(strings::toutf8(src), mode::READ);
	let buf = memio::fixed(strings::toutf8(src));
	let lexer = init(&buf, "<test>");
	const ops: [_]op = [
		op::NEXT,

M hare/lex/lex.ha => hare/lex/lex.ha +13 -13
@@ 10,12 10,12 @@ use bufio;
use encoding::utf8;
use fmt;
use io;
use memio;
use os;
use path;
use sort;
use strconv;
use strings;
use strio;
use types;

export type lexer = struct {


@@ 232,7 232,7 @@ fn lex_rune(lex: *lexer, loc: location) (rune | error) = {

fn lex_string(lex: *lexer, loc: location, delim: rune) (token | error) = {
	let ret: token = (ltok::LIT_STR, "", loc);
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	for (true) match (next(lex)?) {
	case io::EOF =>
		return syntaxerr(loc, "unexpected EOF scanning string literal");


@@ 241,9 241,9 @@ fn lex_string(lex: *lexer, loc: location, delim: rune) (token | error) = {
		else if (delim == '"') {
			unget(lex, r);
			let r = lex_rune(lex, loc)?;
			strio::appendrune(&buf, r)?;
			memio::appendrune(&buf, r)?;
		} else {
			strio::appendrune(&buf, r.0)?;
			memio::appendrune(&buf, r.0)?;
		};
	};
	for (true) match (nextw(lex)?) {


@@ 254,7 254,7 @@ fn lex_string(lex: *lexer, loc: location, delim: rune) (token | error) = {
		case '"', '`' =>
			const tok = lex_string(lex, loc, r.0)?;
			const next = tok.1 as str;
			strio::concat(&buf, next)!;
			memio::concat(&buf, next)!;
			free(next);
			break;
		case '/' =>


@@ 276,7 276,7 @@ fn lex_string(lex: *lexer, loc: location, delim: rune) (token | error) = {
			break;
		};
	};
	return (ltok::LIT_STR, strio::string(&buf), loc);
	return (ltok::LIT_STR, memio::string(&buf)!, loc);
};

fn lex_rn_str(lex: *lexer) (token | error) = {


@@ 310,11 310,11 @@ fn lex_rn_str(lex: *lexer) (token | error) = {
};

fn lex_name(lex: *lexer, loc: location) (token | error) = {
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	match (next(lex)) {
	case let r: (rune, location) =>
		assert(is_name(r.0, false));
		strio::appendrune(&buf, r.0)!;
		memio::appendrune(&buf, r.0)!;
	case (io::EOF | io::error) =>
		abort();
	};


@@ 326,12 326,12 @@ fn lex_name(lex: *lexer, loc: location) (token | error) = {
			unget(lex, r);
			break;
		};
		strio::appendrune(&buf, r.0)?;
		memio::appendrune(&buf, r.0)?;
	};

	line_comment(lex)?;

	let n = strio::string(&buf);
	let n = memio::string(&buf)!;

	match (sort::searchstrings(bmap[..ltok::LAST_KEYWORD+1], n)) {
	case void =>


@@ 383,19 383,19 @@ fn lex_comment(lexr: *lexer) (void | error) = {
		return;
	};

	let buf = strio::dynamic();
	let buf = memio::dynamic();
	defer io::close(&buf)!;
	for (true) match (next(lexr)?) {
	case io::EOF =>
		break;
	case let r: (rune, location) =>
		strio::appendrune(&buf, r.0)!;
		memio::appendrune(&buf, r.0)!;
		if (r.0 == '\n') {
			break;
		};
	};
	let bytes = strings::toutf8(lexr.comment);
	append(bytes, strings::toutf8(strio::string(&buf))...);
	append(bytes, strings::toutf8(memio::string(&buf)!)...);
	lexr.comment = strings::fromutf8(bytes)!;
};


M hare/module/context.ha => hare/module/context.ha +3 -3
@@ 7,10 7,10 @@ use fmt;
use fs;
use glob;
use hare::ast;
use memio;
use os;
use path;
use strings;
use strio;

export type context = struct {
	// Filesystem to use for the cache and source files.


@@ 112,13 112,13 @@ export fn identpath(name: ast::ident) str = {
//
// This is used for module names in environment variables and some file names.
export fn identuscore(ident: ast::ident) str = {
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	for (let i = 0z; i < len(ident); i += 1) {
		fmt::fprintf(&buf, "{}{}", ident[i],
			if (i + 1 < len(ident)) "_"
			else "") as size;
	};
	return strio::string(&buf);
	return memio::string(&buf)!;
};

@test fn identuscore() void = {

M hare/module/manifest.ha => hare/module/manifest.ha +2 -2
@@ 72,7 72,7 @@ export fn manifest_load(ctx: *context, ident: ast::ident) (manifest | error) = {
	let inputs: []input = [], versions: []version = [];

	let buf: [4096]u8 = [0...];
	let file = bufio::buffered(truefile, buf, []);
	let file = bufio::init(truefile, buf, []);
	for (true) {
		let line = match (bufio::scanline(&file)) {
		case io::EOF =>


@@ 324,7 324,7 @@ export fn manifest_write(ctx: *context, man: *manifest) (void | error) = {

	let (truefile, name) = temp::named(ctx.fs, cachedir, io::mode::WRITE, 0o644)?;
	let wbuf: [os::BUFSIZ]u8 = [0...];
	let file = &bufio::buffered(truefile, [], wbuf);
	let file = &bufio::init(truefile, [], wbuf);
	defer {
		bufio::flush(file)!;
		fs::remove(ctx.fs, name): void;

M hare/module/scan.ha => hare/module/scan.ha +5 -5
@@ 13,10 13,10 @@ use hare::lex;
use hare::parse;
use hash;
use io;
use memio;
use path;
use sort;
use strings;
use strio;
use bufio;
use os;



@@ 358,7 358,7 @@ fn scan_file(
	let truef = fs::open(ctx.fs, path)?;
	defer io::close(truef)!;
	let rbuf: [os::BUFSIZ]u8 = [0...];
	let f = &bufio::buffered(truef, rbuf, []);
	let f = &bufio::init(truef, rbuf, []);
	let sha = sha256::sha256();
	hash::write(&sha, strings::toutf8(path));
	hash::write(&sha, [ABI_VERSION]);


@@ 431,19 431,19 @@ export fn parsetags(in: str) ([]tag | void) = {
		case '-' =>
			yield tag_mode::EXCLUSIVE;
		};
		let buf = strio::dynamic();
		let buf = memio::dynamic();
		for (true) match (strings::next(&iter)) {
		case void =>
			break;
		case let r: rune =>
			if (ascii::isalnum(r) || r == '_') {
				strio::appendrune(&buf, r)!;
				memio::appendrune(&buf, r)!;
			} else {
				strings::prev(&iter);
				break;
			};
		};
		t.name = strio::string(&buf);
		t.name = memio::string(&buf)!;
		append(tags, t);
	};
	return tags;

M hare/parse/+test/ident_test.ha => hare/parse/+test/ident_test.ha +6 -6
@@ 1,17 1,17 @@
// License: MPL-2.0
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
use bufio;
use hare::ast;
use hare::lex;
use io;
use io::{mode};
use memio;
use strings;

@test fn ident() void = {
	{
		const in = ";";
		let buf = bufio::fixed(strings::toutf8(in), mode::READ);
		let buf = memio::fixed(strings::toutf8(in));
		let lexer = lex::init(&buf, "<test>");
		defer lex::finish(&lexer);
		ident(&lexer) as error: void;


@@ 21,7 21,7 @@ use strings;

	{
		const in = "foo";
		let buf = bufio::fixed(strings::toutf8(in), mode::READ);
		let buf = memio::fixed(strings::toutf8(in));
		let lexer = lex::init(&buf, "<test>");
		defer lex::finish(&lexer);
		let ident = ident(&lexer) as ast::ident;


@@ 34,7 34,7 @@ use strings;

	{
		const in = "foo::bar";
		let buf = bufio::fixed(strings::toutf8(in), mode::READ);
		let buf = memio::fixed(strings::toutf8(in));
		let lexer = lex::init(&buf, "<test>");
		defer lex::finish(&lexer);
		let ident = ident(&lexer) as ast::ident;


@@ 47,7 47,7 @@ use strings;

	{
		const in = "foo::bar::baz";
		let buf = bufio::fixed(strings::toutf8(in), mode::READ);
		let buf = memio::fixed(strings::toutf8(in));
		let lexer = lex::init(&buf, "<test>");
		defer lex::finish(&lexer);
		let ident = ident(&lexer) as ast::ident;


@@ 61,7 61,7 @@ use strings;

	{
		const in = "foo::bar;";
		let buf = bufio::fixed(strings::toutf8(in), mode::READ);
		let buf = memio::fixed(strings::toutf8(in));
		let lexer = lex::init(&buf, "<test>");
		defer lex::finish(&lexer);
		let ident = ident(&lexer) as ast::ident;

M hare/parse/+test/loc.ha => hare/parse/+test/loc.ha +4 -4
@@ 2,17 2,17 @@
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use bufio;
use encoding::utf8;
use fmt;
use hare::ast;
use hare::lex;
use io;
use io::{mode};
use memio;
use strings;

fn expr_testloc(srcs: str...) void = for (let i = 0z; i < len(srcs); i += 1) {
	let buf = bufio::fixed(strings::toutf8(srcs[i]), mode::READ);
	let buf = memio::fixed(strings::toutf8(srcs[i]));
	let lexer = lex::init(&buf, "<test>");
	defer lex::finish(&lexer);
	let exp = match (expr(&lexer)) {


@@ 77,7 77,7 @@ fn expr_testloc(srcs: str...) void = for (let i = 0z; i < len(srcs); i += 1) {

	// We want to check the location of nested expressions, so this can't
	// use expr_testloc
	let buf = bufio::fixed(strings::toutf8("foo: bar: baz"), mode::READ);
	let buf = memio::fixed(strings::toutf8("foo: bar: baz"));
	let lexer = lex::init(&buf, "<test>");
	defer lex::finish(&lexer);
	let exp = match (expr(&lexer)) {


@@ 101,7 101,7 @@ fn expr_testloc(srcs: str...) void = for (let i = 0z; i < len(srcs); i += 1) {
};

fn type_testloc(srcs: str...) void = for (let i = 0z; i < len(srcs); i += 1) {
	let buf = bufio::fixed(strings::toutf8(srcs[i]), mode::READ);
	let buf = memio::fixed(strings::toutf8(srcs[i]));
	let lexer = lex::init(&buf, "<test>");
	defer lex::finish(&lexer);
	let typ = match (_type(&lexer)) {

M hare/parse/+test/roundtrip.ha => hare/parse/+test/roundtrip.ha +4 -5
@@ 3,15 3,14 @@
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
// (c) 2022 Sebastian <sebastian@sebsite.pw>
use bufio;
use fmt;
use hare::ast;
use hare::lex;
use hare::unparse;
use io::{mode};
use io;
use memio;
use strings;
use strio;

fn roundtrip(src: str) void = {
	let unsrc = _roundtrip(src);


@@ 30,7 29,7 @@ fn roundtrip_reparse(src: str) void = {
};

fn _roundtrip(src: str) str = {
	let buf = bufio::fixed(strings::toutf8(src), mode::READ);
	let buf = memio::fixed(strings::toutf8(src));
	let lexer = lex::init(&buf, "<test>", lex::flag::COMMENTS);
	defer lex::finish(&lexer);
	let u = ast::subunit {


@@ 44,9 43,9 @@ fn _roundtrip(src: str) str = {
		},
	};
	defer ast::subunit_finish(u);
	let out = strio::dynamic();
	let out = memio::dynamic();
	let z = unparse::subunit(&out, u) as size;
	let unsrc = strio::string(&out);
	let unsrc = memio::string(&out)!;
	assert(z == len(unsrc));
	return unsrc;
};

M hare/parse/+test/unit_test.ha => hare/parse/+test/unit_test.ha +3 -3
@@ 4,10 4,10 @@
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
// (c) 2022 Sebastian <sebastian@sebsite.pw>
use bufio;
use hare::ast;
use hare::lex;
use io::{mode};
use memio;
use strings;

fn import_eq(i1: ast::import, i2: ast::import) bool = {


@@ 75,7 75,7 @@ fn tup_to_import(tup: import_tuple) ast::import = ast::import {
		"use modalias = quux::{alias = grault, alias2 = garply};\n"

		"export fn main() void = void;";
	let buf = bufio::fixed(strings::toutf8(in), mode::READ);
	let buf = memio::fixed(strings::toutf8(in));
	let lexer = lex::init(&buf, "<test>");
	defer lex::finish(&lexer);
	let mods = imports(&lexer)!;


@@ 116,7 116,7 @@ fn tup_to_import(tup: import_tuple) ast::import = ast::import {
	assert(tok.0 == lex::ltok::EXPORT);

	const in = "use a::{b = c = d};\n";
	let buf = bufio::fixed(strings::toutf8(in), mode::READ);
	let buf = memio::fixed(strings::toutf8(in));
	let lexer = lex::init(&buf, "<test>");
	defer lex::finish(&lexer);
	assert(imports(&lexer) is error);

M hare/parse/ident.ha => hare/parse/ident.ha +2 -3
@@ 2,11 2,10 @@
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use bufio;
use hare::ast;
use hare::lex::{ltok};
use hare::lex;
use io;
use memio;
use strings;

fn ident_trailing(lexer: *lex::lexer) ((ast::ident, bool) | error) = {


@@ 48,7 47,7 @@ export fn ident(lexer: *lex::lexer) (ast::ident | error) = {
// A convenience function which parses an identifier from a string, so the
// caller needn't provide a lexer instance.
export fn identstr(in: str) (ast::ident | error) = {
	const buf = bufio::fixed(strings::toutf8(in), io::mode::READ);
	const buf = memio::fixed(strings::toutf8(in));
	const lexer = lex::init(&buf, "<string>");
	defer lex::finish(&lexer);
	let ret = ident(&lexer);

M hare/parse/parse.ha => hare/parse/parse.ha +3 -3
@@ 7,7 7,7 @@ use fmt;
use hare::lex::{ltok};
use hare::lex;
use io;
use strio;
use memio;

// All possible error types.
export type error = !lex::error;


@@ 37,7 37,7 @@ fn want(lexer: *lex::lexer, want: lex::ltok...) (lex::token | error) = {
		};
	};

	let buf = strio::dynamic();
	let buf = memio::dynamic();
	defer io::close(&buf)!;
	for (let i = 0z; i < len(want); i += 1) {
		const tstr = if (want[i] == ltok::NAME) "name"


@@ 49,7 49,7 @@ fn want(lexer: *lex::lexer, want: lex::ltok...) (lex::token | error) = {
	};
	lex::unlex(lexer, tok);
	return syntaxerr(lex::mkloc(lexer), "Unexpected '{}', was expecting {}",
		lex::tokstr(tok), strio::string(&buf));
		lex::tokstr(tok), memio::string(&buf)!);
};

// Looks for a matching ltok from the lexer, and if not present, unlexes the

M hare/types/+test.ha => hare/types/+test.ha +3 -3
@@ 4,17 4,17 @@
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
// (c) 2022 Sebastian <sebastian@sebsite.pw>
use bufio;
use errors;
use fmt;
use hare::ast;
use hare::lex;
use hare::parse;
use io;
use memio;
use strings;
use fmt;

fn parse_type(in: str) ast::_type = {
	let buf = bufio::fixed(strings::toutf8(in), io::mode::READ);
	let buf = memio::fixed(strings::toutf8(in));
	let lex = lex::init(&buf, "<test>");
	defer lex::finish(&lex);
	return parse::_type(&lex)!;

M hare/unit/+test.ha => hare/unit/+test.ha +2 -2
@@ 1,16 1,16 @@
// License: MPL-2.0
// (c) 2021-2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
use bufio;
use hare::ast;
use hare::lex;
use hare::parse;
use hare::types;
use io;
use memio;
use strings;

fn parse_expr(src: str) *ast::expr = {
	const stream = bufio::fixed(strings::toutf8(src), io::mode::READ);
	const stream = memio::fixed(strings::toutf8(src));
	const lexer = lex::init(&stream, "<test>");
	defer lex::finish(&lexer);
	return alloc(parse::expr(&lexer)!);

M hare/unparse/decl.ha => hare/unparse/decl.ha +3 -3
@@ 7,8 7,8 @@ use io;
use fmt;
use hare::ast;
use hare::lex;
use memio;
use strings;
use strio;

// Unparses a [[hare::ast::decl]].
export fn decl(out: io::handle, d: ast::decl) (size | io::error) = {


@@ 124,9 124,9 @@ fn comment(out: io::handle, s: str, indent: size) (size | io::error) = {
};

fn decl_test(d: ast::decl, expected: str) bool = {
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	decl(&buf, d) as size;
	let s = strio::string(&buf);
	let s = memio::string(&buf)!;
	defer free(s);
	return s == expected;
};

M hare/unparse/ident.ha => hare/unparse/ident.ha +3 -3
@@ 5,7 5,7 @@
use fmt;
use hare::ast;
use io;
use strio;
use memio;

// Unparses an identifier.
export fn ident(out: io::handle, id: ast::ident) (size | io::error) = {


@@ 20,9 20,9 @@ export fn ident(out: io::handle, id: ast::ident) (size | io::error) = {

// Unparses an identifier into a string. The caller must free the return value.
export fn identstr(id: ast::ident) str = {
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	ident(&buf, id)!;
	return strio::string(&buf);
	return memio::string(&buf)!;
};

@test fn ident() void = {

M hare/unparse/import.ha => hare/unparse/import.ha +3 -3
@@ 5,7 5,7 @@
use fmt;
use io;
use hare::ast;
use strio;
use memio;

// Unparses a [[hare::ast::import]].
export fn import(out: io::handle, import: ast::import) (size | io::error) = {


@@ 81,9 81,9 @@ export fn import(out: io::handle, import: ast::import) (size | io::error) = {
		},  "use quux = foo::{alias1 = bar, alias2 = baz};"),
	];
	for (let i = 0z; i < len(tests); i += 1) {
		let buf = strio::dynamic();
		let buf = memio::dynamic();
		import(&buf, tests[i].0) as size;
		let s = strio::string(&buf);
		let s = memio::string(&buf)!;
		assert(s == tests[i].1);
		free(s);
	};

M hare/unparse/type.ha => hare/unparse/type.ha +9 -9
@@ 7,8 7,8 @@ use io;
use hare::ast;
use hare::ast::{variadism};
use hare::lex;
use memio;
use strings;
use strio;

// Returns a builtin type as a string.
export fn builtin_type(b: ast::builtin_type) str = switch (b) {


@@ 77,22 77,22 @@ export fn prototype(
	// estimate length of prototype to determine if it should span multiple
	// lines
	const linelen = if (len(t.params) == 0) {
		let strm = strio::dynamic();
		let strm = memio::dynamic();
		defer io::close(&strm)!;
		_type(&strm, indent, *t.result)?;
		retname = strings::dup(strio::string(&strm));
		retname = strings::dup(memio::string(&strm)!);
		yield 0z; // only use one line if there's no parameters
	} else {
		let strm = strio::dynamic();
		let strm = memio::dynamic();
		defer io::close(&strm)!;
		let linelen = indent * 8 + 5;
		linelen += if (len(t.params) != 0) len(t.params) * 3 - 1 else 0;
		for (let i = 0z; i < len(t.params); i += 1) {
			const param = t.params[i];
			linelen += _type(&strm, indent, *param._type)?;
			typenames[i] = strings::dup(strio::string(&strm));
			typenames[i] = strings::dup(memio::string(&strm)!);
			linelen += if (param.name == "") -2 else len(param.name);
			strio::reset(&strm);
			memio::reset(&strm);
		};
		switch (t.variadism) {
		case variadism::NONE => void;


@@ 102,7 102,7 @@ export fn prototype(
			linelen += 5;
		};
		linelen += _type(&strm, indent, *t.result)?;
		retname = strings::dup(strio::string(&strm));
		retname = strings::dup(memio::string(&strm)!);
		yield linelen;
	};



@@ 355,9 355,9 @@ export fn _type(
};

fn type_test(t: ast::_type, expected: str) void = {
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	_type(&buf, 0, t) as size;
	let s = strio::string(&buf);
	let s = memio::string(&buf)!;
	defer free(s);
	if (s != expected) {
		fmt::errorfln("=== wanted\n{}", expected)!;

M hash/siphash/+test.ha => hash/siphash/+test.ha +0 -1
@@ 2,7 2,6 @@ use endian;
use fmt;
use hash;
use io;
use strio;
use strings;

@test fn siphash() void = {

A memio/README => memio/README +11 -0
@@ 0,0 1,11 @@
memio provides implementations of [[io::stream]] which can read from or write to
byte slices. [[fixed]] uses a caller-supplied buffer for storage, while
[[dynamic]] uses a dynamically allocated buffer which will grow instead of
erroring when writing past the end of the buffer. All memio streams are
seekable; the read-write head works the same way as an operating system file.
You can access the contents of the buffer via [[buffer]] and [[string]].

Additionally, memio provides string-related I/O operations. Each of the utility
functions (e.g. [[appendrune]]) work correctly with any [[io::handle]], but
for efficiency reasons it is recommended that they are either a memio or
[[bufio]] stream.

R strio/ops.ha => memio/ops.ha +12 -12
@@ 7,7 7,7 @@ use io;
use strings;

// Appends zero or more strings to an [[io::handle]]. The output needn't be a
// strio stream, but it's generally more efficient if it is. Returns the number
// memio stream, but it's generally more efficient if it is. Returns the number
// of bytes written, or an error.
export fn concat(out: io::handle, strs: str...) (size | io::error) =
	join(out, "", strs...);


@@ 27,13 27,13 @@ export fn concat(out: io::handle, strs: str...) (size | io::error) =
	];
	for (let i = 0z; i < len(tests); i += 1) {
		let ln = concat(&st, tests[i].0...) as size;
		assert(ln == len(tests[i].1) && string(&st) == tests[i].1);
		truncate(&st);
		assert(ln == len(tests[i].1) && string(&st)! == tests[i].1);
		reset(&st);
	};
};

// Joins several strings together by a delimiter and writes them to a handle.
// The output needn't be a strio stream, but it's generally more efficient if it
// The output needn't be a memio stream, but it's generally more efficient if it
// is. Returns the number of bytes written, or an error.
export fn join(out: io::handle, delim: str, strs: str...) (size | io::error) = {
	let n = 0z;


@@ 63,13 63,13 @@ export fn join(out: io::handle, delim: str, strs: str...) (size | io::error) = {
	];
	for (let i = 0z; i < len(tests); i += 1) {
		let ln = join(&st, tests[i].0, tests[i].1...) as size;
		assert(ln == len(tests[i].2) && string(&st) == tests[i].2);
		truncate(&st);
		assert(ln == len(tests[i].2) && string(&st)! == tests[i].2);
		reset(&st);
	};
};

// Appends zero or more strings to an [[io::handle]], in reverse order. The
// output needn't be a strio stream, but it's generally more efficient if it is.
// output needn't be a memio stream, but it's generally more efficient if it is.
// Returns the number of bytes written, or an error.
export fn rconcat(out: io::handle, strs: str...) (size | io::error) =
	rjoin(out, "", strs...);


@@ 89,13 89,13 @@ export fn rconcat(out: io::handle, strs: str...) (size | io::error) =
	];
	for (let i = 0z; i < len(tests); i += 1) {
		let ln = rconcat(&st, tests[i].0...) as size;
		assert(ln == len(tests[i].1) && string(&st) == tests[i].1);
		truncate(&st);
		assert(ln == len(tests[i].1) && string(&st)! == tests[i].1);
		reset(&st);
	};
};

// Joins several strings together by a delimiter and writes them to a handle, in
// reverse order. The output needn't be a strio stream, but it's generally more
// reverse order. The output needn't be a memio stream, but it's generally more
// efficient if it is. Returns the number of bytes written, or an error.
export fn rjoin(out: io::handle, delim: str, strs: str...) (size | io::error) = {
	let n = 0z;


@@ 125,8 125,8 @@ export fn rjoin(out: io::handle, delim: str, strs: str...) (size | io::error) = 
	];
	for (let i = 0z; i < len(tests); i += 1) {
		let ln = rjoin(&st, tests[i].0, tests[i].1...) as size;
		assert(ln == len(tests[i].2) && string(&st) == tests[i].2);
		truncate(&st);
		assert(ln == len(tests[i].2) && string(&st)! == tests[i].2);
		reset(&st);
	};
};


A memio/stream.ha => memio/stream.ha +274 -0
@@ 0,0 1,274 @@
// License: MPL-2.0
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Bor Grošelj Simić <bor.groseljsimic@telemach.net>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use bytes;
use encoding::utf8;
use errors;
use io;
use strings;

export type stream = struct {
	stream: io::stream,
	buf: []u8,
	pos: size,
};

const fixed_vt: io::vtable = io::vtable {
	seeker = &seek,
	copier = &copy,
	reader = &read,
	writer = &fixed_write,
	...
};

const dynamic_vt: io::vtable = io::vtable {
	seeker = &seek,
	copier = &copy,
	reader = &read,
	writer = &dynamic_write,
	closer = &dynamic_close,
	...
};

// Creates a stream for a fixed, caller-supplied buffer. Seeking a stream will
// cause subsequent writes to overwrite existing contents of the buffer.
// Writes return an error if they would exceed the buffer's capacity. The
// stream doesn't have to be closed.
export fn fixed(in: []u8) stream = stream {
	stream = &fixed_vt,
	buf = in,
	pos = 0,
};

// Creates an [[io::stream]] which dynamically allocates a buffer to store
// writes into. Seeking the stream and reading will read the written data.
// Calling [[io::close]] on this stream will free the buffer. If a stream's
// data is referenced via [[buffer]], the stream shouldn't be closed as
// long as the data is used.
export fn dynamic() stream = dynamic_from([]);

// Like [[dynamic]], but takes an existing slice as input. Writes will
// overwrite the buffer and reads consume bytes from the initial buffer.
// Like [[dynamic]], calling [[io::close]] will free the buffer.
export fn dynamic_from(in: []u8) stream = stream {
	stream = &dynamic_vt,
	buf = in,
	pos = 0,
};

// Returns a stream's buffer, up to the current cursor position.
// [[io::seek]] to the end first in order to return the entire buffer.
// The return value is borrowed from the input.
export fn buffer(in: *stream) []u8 = {
	return in.buf[..in.pos];
};

// Returns a stream's buffer, up to the current cursor position, as a string.
// [[io::seek]] to the end first in order to return the entire buffer.
// The return value is borrowed from the input.
export fn string(in: *stream) (str | utf8::invalid) = {
	return strings::fromutf8(in.buf[..in.pos]);
};

// A convenience function that sets the read-write cursor to zero, so that
// the buffer can be overwritten and reused.
export fn reset(in: *stream) void = {
	in.pos = 0;
};

// Reads data from a [[dynamic]] or [[fixed]] stream and returns a slice
// borrowed from the internal buffer.
export fn borrowedread(st: *stream, amt: size) ([]u8 | io::EOF) = {
	if (len(st.buf) - st.pos < amt) {
		return io::EOF;
	};
	let buf = st.buf[st.pos..st.pos + amt];
	st.pos += len(buf);
	return buf;
};

fn read(s: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
	let s = s: *stream;
	if (len(s.buf) == s.pos && len(buf) != 0) {
		return io::EOF;
	};
	const n = if (len(s.buf) - s.pos < len(buf)) {
		yield len(s.buf) - s.pos;
	} else {
		yield len(buf);
	};
	assert(s.pos + n <= len(s.buf));
	buf[..n] = s.buf[s.pos..s.pos + n];
	s.pos += n;
	return n;
};

fn seek(
	s: *io::stream,
	off: io::off,
	w: io::whence
) (io::off | io::error) = {
	let s = s: *stream;
	let start = switch (w) {
	case io::whence::SET => yield 0z;
	case io::whence::CUR => yield s.pos;
	case io::whence::END => yield len(s.buf);
	};
	if (off < 0) {
		if (start < (-off): size) return errors::invalid;
	} else {
		if (len(s.buf) - start < off: size) return errors::invalid;
	};
	s.pos = start + off: size;
	return s.pos: io::off;
};

fn copy(dest: *io::stream, src: *io::stream) (size | io::error) = {
	if (src.reader != &read || dest.writer == null) {
		return errors::unsupported;
	};
	let src = src: *stream;
	return (dest.writer: *io::writer)(dest, src.buf[src.pos..]);
};

fn fixed_write(s: *io::stream, buf: const []u8) (size | io::error) = {
	if (len(buf) == 0) {
		return 0z;
	};
	let s = s: *stream;
	if (s.pos >= len(s.buf)) {
		return errors::overflow;
	};
	const n = if (len(buf) > len(s.buf[s.pos..])) {
		yield len(s.buf[s.pos..]);
	} else {
		yield len(buf);
	};
	s.buf[s.pos..s.pos+n] = buf[..n];
	s.pos += n;
	return n;
};

fn dynamic_write(s: *io::stream, buf: const []u8) (size | io::error) = {
	let s = s: *stream;
	let spare = len(s.buf) - s.pos;
	let bufend = if (spare < len(buf)) spare else len(buf);
	s.buf[s.pos..s.pos+bufend] = buf[..bufend];
	s.pos += bufend;
	if (bufend < len(buf)) {
		append(s.buf, buf[bufend..]...);
		s.pos += len(buf[bufend..]);
	};
	return len(buf);
};

fn dynamic_close(s: *io::stream) (void | io::error) = {
	const s = s: *stream;
	free(s.buf);
	s.buf = [];
	s.pos = 0;
};

@test fn fixed() void = {
	let buf: [1024]u8 = [0...];
	let stream = fixed(buf);
	defer io::close(&stream)!;

	let n = 0z;
	n += io::writeall(&stream, strings::toutf8("hello ")) as size;
	n += io::writeall(&stream, strings::toutf8("world")) as size;
	assert(bytes::equal(buf[..n], strings::toutf8("hello world")));
	assert(io::seek(&stream, 6, io::whence::SET) as io::off == 6: io::off);
	io::writeall(&stream, strings::toutf8("asdf")) as size;
	assert(bytes::equal(buf[..n], strings::toutf8("hello asdfd")));

	let out: [2]u8 = [0...];
	let s = fixed([1u8, 2u8]);
	defer io::close(&s)!;
	assert(io::read(&s, out[..1]) as size == 1 && out[0] == 1);
	assert(io::seek(&s, 1, io::whence::CUR) as io::off == 2: io::off);
	assert(io::read(&s, buf[..]) is io::EOF);
	assert(io::writeall(&s, [1, 2]) as io::error is errors::overflow);

	let in: [6]u8 = [0, 1, 2, 3, 4, 5];
	let out: [6]u8 = [0...];
	let source = fixed(in);
	let sink = fixed(out);
	io::copy(&sink, &source)!;
	assert(bytes::equal(in, out));

	assert(io::write(&sink, [])! == 0);

	static let buf: [1024]u8 = [0...];
	let stream = fixed(buf);
	assert(string(&stream)! == "");
	io::writeall(&stream, strings::toutf8("hello ")) as size;
	assert(string(&stream)! == "hello ");
	io::writeall(&stream, strings::toutf8("world")) as size;
	assert(string(&stream)! == "hello world");
};

@test fn dynamic() void = {
	let s = dynamic();
	assert(io::writeall(&s, [1, 2, 3]) as size == 3);
	assert(bytes::equal(buffer(&s), [1, 2, 3]));
	assert(io::writeall(&s, [4, 5]) as size == 2);
	assert(bytes::equal(buffer(&s), [1, 2, 3, 4, 5]));
	let buf: [2]u8 = [0...];
	assert(io::seek(&s, 0, io::whence::SET) as io::off == 0: io::off);
	assert(io::read(&s, buf[..]) as size == 2 && bytes::equal(buf, [1, 2]));
	assert(io::read(&s, buf[..]) as size == 2 && bytes::equal(buf, [3, 4]));
	assert(io::read(&s, buf[..]) as size == 1 && buf[0] == 5);
	assert(io::read(&s, buf[..]) is io::EOF);
	assert(io::writeall(&s, [6, 7, 8]) as size == 3);
	assert(bytes::equal(buffer(&s), [1, 2, 3, 4, 5, 6, 7, 8]));
	reset(&s);
	assert(len(buffer(&s)) == 0);
	assert(io::writeall(&s, [1, 2, 3]) as size == 3);
	assert(io::close(&s) is void);

	let sl: []u8 = alloc([1, 2, 3]);
	let s = dynamic_from(sl);
	assert(io::writeall(&s, [0, 0]) as size == 2);
	assert(io::seek(&s, 0, io::whence::END) as io::off == 3: io::off);
	assert(io::writeall(&s, [4, 5, 6]) as size == 3);
	assert(bytes::equal(buffer(&s), [0, 0, 3, 4, 5, 6]));
	assert(io::read(&s, buf[..]) is io::EOF);
	io::close(&s)!;

	sl = alloc([1, 2]);
	let s = dynamic_from(sl);
	assert(io::read(&s, buf[..1]) as size == 1 && buf[0] == 1);
	assert(io::seek(&s, 1, io::whence::CUR) as io::off == 2: io::off);
	assert(io::read(&s, buf[..]) is io::EOF);
	assert(io::writeall(&s, [3, 4]) as size == 2 && bytes::equal(buffer(&s), [1, 2, 3, 4]));
	io::close(&s)!;
	assert(io::writeall(&s, [5, 6]) as size == 2 && bytes::equal(buffer(&s), [5, 6]));
	io::close(&s)!;

	let in: [6]u8 = [0, 1, 2, 3, 4, 5];
	let source = dynamic_from(in);
	let sink = dynamic();
	io::copy(&sink, &source)!;
	assert(bytes::equal(in, buffer(&sink)));

	let in: [6]u8 = [0, 1, 2, 3, 4, 5];
	let source = dynamic_from(in);
	const borrowed = borrowedread(&source, len(in)-1) as []u8;
	assert(bytes::equal(borrowed, [0, 1, 2, 3, 4]));
	let source = dynamic_from(in);
	const borrowed = borrowedread(&source, len(in)) as []u8;
	assert(bytes::equal(borrowed, [0, 1, 2, 3, 4, 5]));
	let source = dynamic_from(in);
	assert(borrowedread(&source, len(in)+1) is io::EOF);

	let stream = dynamic();
	defer io::close(&stream)!;
	assert(string(&stream)! == "");
	io::writeall(&stream, strings::toutf8("hello ")) as size;
	assert(string(&stream)! == "hello ");
	io::writeall(&stream, strings::toutf8("world")) as size;
	assert(string(&stream)! == "hello world");
};

M mime/system.ha => mime/system.ha +1 -1
@@ 21,7 21,7 @@ fn load_systemdb() (void | fs::error | io::error) = {
	const file = os::open(SYSTEM_DB)?;

	let buf: [os::BUFSIZ]u8 = [0...];
	const strm = bufio::buffered(file, buf, []);
	const strm = bufio::init(file, buf, []);

	for (true) {
		const line = match (bufio::scanline(&strm)) {

M net/ip/ip.ha => net/ip/ip.ha +3 -3
@@ 11,9 11,9 @@ use bytes;
use endian;
use fmt;
use io;
use memio;
use strconv;
use strings;
use strio;

// An IPv4 address.
export type addr4 = [4]u8;


@@ 341,9 341,9 @@ export fn fmt(s: io::handle, item: (...addr | subnet)) (size | io::error) = {
export fn string(item: (...addr | subnet)) str = {
	// Maximum length of an IPv6 address plus its netmask in hexadecimal
	static let buf: [64]u8 = [0...];
	let stream = strio::fixed(buf);
	let stream = memio::fixed(buf);
	fmt(&stream, item) as size;
	return strio::string(&stream);
	return memio::string(&stream)!;
};

fn wanttoken(tok: *strings::tokenizer) (str | invalid) = {

M net/uri/fmt.ha => net/uri/fmt.ha +3 -3
@@ 2,10 2,10 @@ use ascii;
use encoding::utf8;
use fmt;
use io;
use memio;
use net::ip;
use strconv;
use strings;
use strio;


// Extract from RFC3986 ABNF


@@ 114,7 114,7 @@ fn percent_encode(out: io::handle, src: str, allowed: str) (size | io::error) = 

// Formats a [[uri]] into a string. The result must be freed by the caller.
export fn string(u: *const uri) str = {
	const st = strio::dynamic();
	const st = memio::dynamic();
	fmt(&st, u)!;
	return strio::string(&st);
	return memio::string(&st)!;
};

M net/uri/parse.ha => net/uri/parse.ha +22 -22
@@ 4,10 4,10 @@
use ascii;
use encoding::utf8;
use io;
use memio;
use net::ip;
use strconv;
use strings;
use strio;

// The URI provided to [[parse]] is invalid.
export type invalid = !void;


@@ 132,7 132,7 @@ fn parse_authority(
	in: *strings::iterator,
) (((str | ip::addr6), u16, str) | invalid) = {
	// Scan everything until '@' or ':' or '/', then decide what it is
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	defer io::close(&buf)!;
	let host: (str | ip::addr6) = "";
	let port = 0u16;


@@ 148,25 148,25 @@ fn parse_authority(
		};

		if (r == '[') {
			if (len(strio::string(&buf)) > 0) {
			if (len(memio::string(&buf)!) > 0) {
				if (len(userinfo) > 0) {
					return invalid;
				} else {
					userinfo = percent_decode(
						strio::string(&buf))?;
						memio::string(&buf)!)?;
				};
			};
			strio::reset(&buf);
			memio::reset(&buf);

			for (true) {
				const r = wantrune(in)?;
				if (r == ']') {
					break;
				};
				strio::appendrune(&buf, r)!;
				memio::appendrune(&buf, r)!;
			};

			const addr = percent_decode(strio::string(&buf))?;
			const addr = percent_decode(memio::string(&buf)!)?;
			match (ip::parse(addr)) {
			case let v6: ip::addr6 =>
				host = v6;


@@ 180,24 180,24 @@ fn parse_authority(
					return invalid;
				};
				// This was userinfo+host[+port]
				userinfo = percent_decode(strio::string(&buf))?;
				strio::reset(&buf);
				userinfo = percent_decode(memio::string(&buf)!)?;
				memio::reset(&buf);
				has_userinfo = true;
			case '/' =>
				// This was just host
				strings::prev(in);
				host = percent_decode(strio::string(&buf))?;
				host = percent_decode(memio::string(&buf)!)?;
				break;
			case ':' =>
				// This was host+port
				host = percent_decode(strio::string(&buf))?;
				host = percent_decode(memio::string(&buf)!)?;
				port = parse_port(in)?;
				break;
			case =>
				return invalid;
			};
		} else {
			strio::appendrune(&buf, r)!;
			memio::appendrune(&buf, r)!;
		};
	};



@@ 205,7 205,7 @@ fn parse_authority(
	case let s: str =>
		// In end of string case
		if (len(s) == 0) {
			host = percent_decode(strio::string(&buf))?;
			host = percent_decode(memio::string(&buf)!)?;
		};
	case => yield;
	};


@@ 327,27 327,27 @@ fn parse_port(in: *strings::iterator) (u16 | invalid) = {
};

fn percent_decode(s: str) (str | invalid) = {
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	percent_decode_static(&buf, s)?;
	return strio::string(&buf);
	return memio::string(&buf)!;
};

fn percent_decode_static(out: io::handle, s: str) (void | invalid) = {
	let iter = strings::iter(s);
	let tmp = strio::dynamic();
	let tmp = memio::dynamic();
	defer io::close(&tmp)!;
	let percent_data: []u8 = [];
	for (true) {
		match (strings::next(&iter)) {
		case let r: rune =>
			if (r == '%') {
				strio::reset(&tmp);
				memio::reset(&tmp);
				for (let i = 0z; i < 2; i += 1) {
					const r = wantrune(&iter)?;
					strio::appendrune(&tmp, r)!;
					memio::appendrune(&tmp, r)!;
				};

				match (strconv::stou8b(strio::string(&tmp),
				match (strconv::stou8b(memio::string(&tmp)!,
					strconv::base::HEX)) {
				case let ord: u8 =>
					append(percent_data, ord);


@@ 358,7 358,7 @@ fn percent_decode_static(out: io::handle, s: str) (void | invalid) = {
				if(len(percent_data) > 0) {
					match(strings::fromutf8(percent_data)) {
					case let stro: str =>
						strio::concat(out, stro)!;
						memio::concat(out, stro)!;
					case utf8::invalid =>
						return invalid;
					};


@@ 366,13 366,13 @@ fn percent_decode_static(out: io::handle, s: str) (void | invalid) = {
					percent_data = [];
				};

				strio::appendrune(out, r)!;
				memio::appendrune(out, r)!;
			};
		case void =>
			if(len(percent_data) > 0) {
				match(strings::fromutf8(percent_data)) {
				case let stro: str =>
					strio::concat(out, stro)!;
					memio::concat(out, stro)!;
				case utf8::invalid =>
					return invalid;
				};

M net/uri/query.ha => net/uri/query.ha +11 -11
@@ 1,17 1,17 @@
use io;
use memio;
use strings;
use strio;

export type query_decoder = struct {
	tokenizer: strings::tokenizer,
	bufs: (strio::stream, strio::stream),
	bufs: (memio::stream, memio::stream),
};

// Initializes a decoder for a query string. Use [[query_next]] to walk it. The
// caller must call [[query_finish]] once they're done using it.
export fn decodequery(q: const str) query_decoder = query_decoder {
	tokenizer = strings::tokenize(q, "&"),
	bufs = (strio::dynamic(), strio::dynamic()),
	bufs = (memio::dynamic(), memio::dynamic()),
};

// Frees resources associated with the [[query_decoder]].


@@ 31,33 31,33 @@ export fn query_next(dec: *query_decoder) ((str, str) | invalid | void) = {
	};

	const raw = strings::cut(tok, "=");
	strio::reset(&dec.bufs.0);
	memio::reset(&dec.bufs.0);
	percent_decode_static(&dec.bufs.0, raw.0)?;
	strio::reset(&dec.bufs.1);
	memio::reset(&dec.bufs.1);
	percent_decode_static(&dec.bufs.1, raw.1)?;
	return (
		strio::string(&dec.bufs.0),
		strio::string(&dec.bufs.1),
		memio::string(&dec.bufs.0)!,
		memio::string(&dec.bufs.1)!,
	);
};

// Encodes (key, value) pairs into a URI query string. The result must be
// freed by the caller.
export fn encodequery(pairs: [](str, str)) str = {
	const buf = strio::dynamic();
	const buf = memio::dynamic();
	for (let i = 0z; i < len(pairs); i += 1) {
		const pair = pairs[i];
		if (i > 0) strio::appendrune(&buf, '&')!;
		if (i > 0) memio::appendrune(&buf, '&')!;

		assert(len(pair.0) > 0);
		percent_encode(&buf, pair.0, unres_query_frag)!;
		if (len(pair.1) > 0) {
			strio::appendrune(&buf, '=')!;
			memio::appendrune(&buf, '=')!;
			percent_encode(&buf, pair.1, unres_query_frag)!;
		};
	};

	return strio::string(&buf);
	return memio::string(&buf)!;
};

@test fn decodequery() void = {

M os/+freebsd/stdfd.ha => os/+freebsd/stdfd.ha +4 -4
@@ 5,14 5,14 @@ use bufio;
use io;
use rt;

let stdin_bufio: bufio::bufstream = bufio::bufstream {
let stdin_bufio: bufio::stream = bufio::stream {
	// Will be overwritten, but must be initialized
	stream = null: io::stream,
	source = 0,
	...
};

let stdout_bufio: bufio::bufstream = bufio::bufstream {
let stdout_bufio: bufio::stream = bufio::stream {
	// Will be overwritten, but must be initialized
	stream = null: io::stream,
	source = 1,


@@ 42,11 42,11 @@ export def BUFSIZ: size = 4096; // 4 KiB

@init fn init_stdfd() void = {
	static let stdinbuf: [BUFSIZ]u8 = [0...];
	stdin_bufio = bufio::buffered(stdin_file, stdinbuf, []);
	stdin_bufio = bufio::init(stdin_file, stdinbuf, []);
	stdin = &stdin_bufio;

	static let stdoutbuf: [BUFSIZ]u8 = [0...];
	stdout_bufio = bufio::buffered(stdout_file, [], stdoutbuf);
	stdout_bufio = bufio::init(stdout_file, [], stdoutbuf);
	stdout = &stdout_bufio;
};


M os/+linux/stdfd.ha => os/+linux/stdfd.ha +4 -4
@@ 5,14 5,14 @@ use bufio;
use io;
use rt;

let stdin_bufio: bufio::bufstream = bufio::bufstream {
let stdin_bufio: bufio::stream = bufio::stream {
	// Will be overwritten, but must be initialized
	stream = null: io::stream,
	source = 0,
	...
};

let stdout_bufio: bufio::bufstream = bufio::bufstream {
let stdout_bufio: bufio::stream = bufio::stream {
	// Will be overwritten, but must be initialized
	stream = null: io::stream,
	source = 1,


@@ 42,11 42,11 @@ export def BUFSIZ: size = 4096; // 4 KiB

@init fn init_stdfd() void = {
	static let stdinbuf: [BUFSIZ]u8 = [0...];
	stdin_bufio = bufio::buffered(stdin_file, stdinbuf, []);
	stdin_bufio = bufio::init(stdin_file, stdinbuf, []);
	stdin = &stdin_bufio;

	static let stdoutbuf: [BUFSIZ]u8 = [0...];
	stdout_bufio = bufio::buffered(stdout_file, [], stdoutbuf);
	stdout_bufio = bufio::init(stdout_file, [], stdoutbuf);
	stdout = &stdout_bufio;
};


M regex/regex.ha => regex/regex.ha +4 -3
@@ 5,6 5,7 @@ use bufio;
use encoding::utf8;
use errors;
use io;
use memio;
use strconv;
use strings;
use types;


@@ 773,7 774,7 @@ fn search(

// Returns whether or not a [[regex]] matches any part of a given string.
export fn test(re: *regex, string: str) bool = {
	let strm = bufio::fixed(strings::toutf8(string), io::mode::READ);
	let strm = memio::fixed(strings::toutf8(string));
	return search(re, string, &strm, false) is []capture;
};



@@ 782,7 783,7 @@ export fn test(re: *regex, string: str) bool = {
// leftmost match as a [[result]]. The caller must free the return value with
// [[result_free]].
export fn find(re: *regex, string: str) result = {
	let strm = bufio::fixed(strings::toutf8(string), io::mode::READ);
	let strm = memio::fixed(strings::toutf8(string));
	match (search(re, string, &strm, true)) {
	case let m: []capture =>
		return m;


@@ 798,7 799,7 @@ export fn findall(re: *regex, string: str) []result = {
	let res: [][]capture = [];
	let str_idx = 0z, str_bytesize = 0z;
	let substring = string;
	let strm = bufio::fixed(strings::toutf8(string), io::mode::READ);
	let strm = memio::fixed(strings::toutf8(string));
	const str_bytes = strings::toutf8(string);
	for (true) {
		match (search(re, substring, &strm, true)) {

M scripts/gen-stdlib => scripts/gen-stdlib +169 -127
@@ 161,7 161,7 @@ test() {
	else
		gen_srcs test common.ha +test.ha fail+test.ha
		gen_ssa test bufio encoding::hex encoding::utf8 fmt fnmatch io \
			os rt strings strio time unix::signal
			os rt strings memio time unix::signal
	fi
}



@@ 174,11 174,19 @@ ascii() {
}

bufio() {
	gen_srcs bufio \
		buffered.ha \
		memstream.ha \
		scanner.ha
	gen_ssa bufio io bytes strings encoding::utf8 errors types
	if [ $testing -eq 0 ]; then
		gen_srcs bufio \
			stream.ha \
			scanner.ha
		gen_ssa bufio bytes encoding::utf8 errors io strings types
	else
		gen_srcs bufio \
			stream.ha \
			scanner.ha \
			stream_test+test.ha \
			scanner_test+test.ha
		gen_ssa bufio bytes encoding::utf8 errors io memio strings types
	fi
}

bytes() {


@@ 200,15 208,15 @@ crypto() {
		gen_srcs crypto \
			authenc.ha \
			keyderiv.ha
		gen_ssa crypto bufio bytes crypto::argon2 crypto::chachapoly \
			crypto::math endian errors io
		gen_ssa crypto bytes crypto::argon2 crypto::chachapoly \
			crypto::math endian errors io memio
	else
		gen_srcs crypto \
			authenc.ha \
			keyderiv.ha \
			+test/authenc_test.ha
		gen_ssa crypto bufio bytes crypto::argon2 crypto::chachapoly \
			crypto::math endian errors io
		gen_ssa crypto bytes crypto::argon2 crypto::chachapoly \
			crypto::math endian errors io memio
	fi
}



@@ 233,8 241,8 @@ crypto_aes() {
			ctr+test.ha \
			rt+test.ha \
			+test/gcm.ha
		gen_ssa crypto::aes bufio bytes crypto::cipher crypto::math \
			endian errors io rt
		gen_ssa crypto::aes bytes crypto::cipher crypto::math \
			endian errors io memio rt 
	fi
}



@@ 252,13 260,13 @@ crypto_argon2() {
	if [ $testing -eq 0 ]
	then
		gen_srcs crypto::argon2 argon2.ha
		gen_ssa crypto::argon2 bufio bytes crypto::blake2b \
			crypto::math endian errors hash io rt types
		gen_ssa crypto::argon2 bytes crypto::blake2b \
			crypto::math endian errors hash io memio rt types
	else
		gen_srcs crypto::argon2 argon2.ha +test.ha
		gen_ssa crypto::argon2 bufio bytes crypto::blake2b \
			crypto::math encoding::hex endian errors hash io rt \
			strings types
		gen_ssa crypto::argon2 bytes crypto::blake2b \
			crypto::math encoding::hex endian errors hash io memio \
			rt strings types
	fi
}



@@ 269,9 277,9 @@ crypto_bcrypt() {
	else
		gen_srcs crypto::bcrypt bcrypt.ha base64.ha +test.ha
	fi
	gen_ssa crypto::bcrypt crypto::blowfish encoding::base64 bufio io \
		crypto crypto::random errors crypto::cipher strings fmt bytes \
		strconv
	gen_ssa crypto::bcrypt bytes crypto crypto::blowfish \
		crypto::cipher crypto::random encoding::base64 errors \
		fmt io memio strconv strings
}

gensrcs_crypto_blake2b() {


@@ 288,7 296,7 @@ crypto_blake2b() {
	else
		gensrcs_crypto_blake2b +test.ha vectors+test.ha
		gen_ssa crypto::blake2b encoding::hex fmt hash io strings \
			strio crypto::math endian bytes
			memio crypto::math endian bytes
	fi
}



@@ 335,7 343,7 @@ crypto_chacha() {
	else
		gen_srcs crypto::chacha chacha20.ha +test.ha
		gen_ssa crypto::chacha bytes crypto::cipher crypto::math \
			endian io bufio
			endian io memio
	fi
}



@@ 438,7 446,7 @@ gensrcs_crypto_rsa() {
genssa_crypto_rsa() {
	gen_ssa crypto::rsa bufio bytes crypto::bigint crypto::math \
		crypto::sha1 crypto::sha256 crypto::sha512 endian errors hash \
		io types $*
		io memio types $*
}

crypto_rsa() {


@@ 461,8 469,8 @@ crypto_salsa() {
			io
	else
		gen_srcs crypto::salsa salsa20.ha +test.ha
		gen_ssa crypto::salsa bytes bufio crypto::cipher crypto::math \
			endian types io
		gen_ssa crypto::salsa bytes crypto::cipher crypto::math \
			endian io memio types
	fi
}



@@ 544,6 552,41 @@ crypto_x25519() {
	fi
}

datetime() {
	gen_srcs -plinux datetime \
		arithmetic.ha \
		chronology.ha \
		errors.ha \
		date.ha \
		datetime.ha \
		duration.ha \
		format.ha \
		parse.ha \
		period.ha \
		reckon.ha \
		time.ha \
		timezone.ha \
		virtual.ha
	gen_ssa -plinux datetime ascii errors fmt io strconv strings memio \
		time time::chrono
	gen_srcs -pfreebsd datetime \
		arithmetic.ha \
		chronology.ha \
		errors.ha \
		date.ha \
		datetime.ha \
		duration.ha \
		format.ha \
		parse.ha \
		period.ha \
		reckon.ha \
		time.ha \
		timezone.ha \
		virtual.ha
	gen_ssa -pfreebsd datetime ascii errors fmt io strconv strings memio \
		time time::chrono
}

dirs() {
	gen_srcs dirs \
		xdg.ha


@@ 553,19 596,19 @@ dirs() {
encoding_base64() {
	gen_srcs encoding::base64 \
		base64.ha
	gen_ssa encoding::base64 ascii bufio bytes errors io os strings
	gen_ssa encoding::base64 ascii bytes errors io os strings memio
}

encoding_base32() {
	gen_srcs encoding::base32 \
		base32.ha
	gen_ssa encoding::base32 ascii bufio bytes errors io strings os
	gen_ssa encoding::base32 ascii bytes errors io strings os memio
}

encoding_hex() {
	gen_srcs encoding::hex \
		hex.ha
	gen_ssa encoding::hex ascii bufio bytes errors fmt io strconv strio \
	gen_ssa encoding::hex ascii bytes errors fmt io strconv memio \
		strings
}



@@ 574,13 617,13 @@ encoding_pem() {
	then
		gen_srcs encoding::pem \
			pem.ha
		gen_ssa encoding::pem strings bufio strio io errors \
		gen_ssa encoding::pem strings memio io errors \
			encoding::base64 ascii os fmt
	else
		gen_srcs encoding::pem \
			pem.ha \
			+test.ha
		gen_ssa encoding::pem strings bufio strio io errors \
		gen_ssa encoding::pem strings memio io errors \
			encoding::base64 ascii os fmt bytes
	fi
}


@@ 616,7 659,7 @@ errors() {
fmt() {
	gen_srcs fmt \
		fmt.ha
	gen_ssa fmt ascii bufio encoding::utf8 io os strconv strings types
	gen_ssa fmt ascii encoding::utf8 io memio os strconv strings types
}

fnmatch() {


@@ 651,14 694,14 @@ format_ini() {
	else
		gensrcs_format_ini +test.ha
	fi
	gen_ssa format::ini bufio encoding::utf8 fmt io strings
	gen_ssa format::ini encoding::utf8 fmt io memio strings
}

format_tar() {
	gen_srcs format::tar \
		types.ha \
		reader.ha
	gen_ssa format::tar bufio bytes endian errors io strconv strio types::c
	gen_ssa format::tar bytes endian errors io strconv memio types::c
}

fs() {


@@ 682,7 725,7 @@ glob() {
	else
		gen_srcs glob glob.ha +test.ha
	fi
	gen_ssa glob fnmatch fs io os sort strings strio
	gen_ssa glob fnmatch fs io os sort strings memio
}

hare_ast() {


@@ 696,6 739,69 @@ hare_ast() {
	gen_ssa hare::ast hare::lex strings
}

gensrcs_hare_lex() {
	gen_srcs hare::lex \
		token.ha \
		lex.ha \
		$*
}

hare_lex() {
	if [ $testing -eq 0 ]
	then
		gensrcs_hare_lex
	else
		gensrcs_hare_lex \
			+test.ha
	fi
	gen_ssa hare::lex ascii io encoding::utf8 fmt memio sort \
		strconv strings path
}

hare_module() {
	gen_srcs hare::module \
		types.ha \
		context.ha \
		scan.ha \
		manifest.ha \
		walk.ha
	gen_ssa hare::module \
		hare::ast hare::lex hare::parse hare::unparse memio fs io strings hash \
		crypto::sha256 dirs bytes encoding::utf8 ascii fmt time bufio \
		strconv os encoding::hex sort errors temp path
}

gensrcs_hare_parse() {
	gen_srcs hare::parse \
		decl.ha \
		expr.ha \
		ident.ha \
		import.ha \
		parse.ha \
		type.ha \
		unit.ha \
		$*
}

hare_parse() {
	if [ $testing -eq 0 ]
	then
		gensrcs_hare_parse
		gen_ssa hare::parse ascii hare::ast hare::lex fmt types \
			strings math
	else
		gensrcs_hare_parse \
			+test/expr_test.ha \
			+test/ident_test.ha \
			+test/loc.ha \
			+test/roundtrip.ha \
			+test/types.ha \
			+test/unit_test.ha
		gen_ssa hare::parse ascii memio hare::ast hare::lex \
			hare::unparse io fmt types strings math encoding::utf8
	fi
}

gensrcs_hare_types() {
	gen_srcs hare::types \
		'+$(ARCH)/writesize.ha' \


@@ 714,11 820,11 @@ hare_types() {
	then
		gensrcs_hare_types +test.ha
		gen_ssa hare::types hare::ast hash hash::fnv endian strings \
			errors sort fmt bufio hare::lex hare::parse io
			errors sort fmt hare::lex hare::parse io
	else
		gensrcs_hare_types
		gen_ssa hare::types hare::ast hash hash::fnv endian strings \
			errors sort fmt
			errors memio sort fmt
	fi
}



@@ 740,11 846,11 @@ hare_unit() {
	then
		gensrcs_hare_unit +test.ha
		gen_ssa hare::unit hare::ast hare::types hash hash::fnv \
			strings hare::lex bufio hare::parse
			strings hare::lex hare::parse memio
	else
		gensrcs_hare_unit
		gen_ssa hare::unit hare::ast hare::types hash hash::fnv \
			strings hare::lex
			strings hare::lex memio
	fi
}



@@ 757,71 863,7 @@ hare_unparse() {
		type.ha \
		unit.ha \
		util.ha
	gen_ssa hare::unparse fmt io strings strio hare::ast hare::lex
}

gensrcs_hare_lex() {
	gen_srcs hare::lex \
		token.ha \
		lex.ha \
		$*
}

hare_lex() {
	if [ $testing -eq 0 ]
	then
		gensrcs_hare_lex
	else
		gensrcs_hare_lex \
			+test.ha
	fi
	gen_ssa hare::lex ascii io bufio encoding::utf8 strings fmt sort strio \
		strconv path
}

hare_module() {
	gen_srcs hare::module \
		types.ha \
		context.ha \
		scan.ha \
		manifest.ha \
		walk.ha
	gen_ssa hare::module \
		hare::ast hare::lex hare::parse hare::unparse strio fs io strings hash \
		crypto::sha256 dirs bytes encoding::utf8 ascii fmt time bufio \
		strconv os encoding::hex sort errors temp path
}

gensrcs_hare_parse() {
	gen_srcs hare::parse \
		decl.ha \
		expr.ha \
		ident.ha \
		import.ha \
		parse.ha \
		type.ha \
		unit.ha \
		$*
}

hare_parse() {
	if [ $testing -eq 0 ]
	then
		gensrcs_hare_parse
		gen_ssa hare::parse ascii hare::ast hare::lex fmt types \
			strings math
	else
		gensrcs_hare_parse \
			+test/expr_test.ha \
			+test/ident_test.ha \
			+test/loc.ha \
			+test/roundtrip.ha \
			+test/types.ha \
			+test/unit_test.ha
		gen_ssa hare::parse ascii bufio hare::ast hare::lex \
			hare::unparse io strio fmt types strings math \
			encoding::utf8
	fi
	gen_ssa hare::unparse fmt io strings memio hare::ast hare::lex
}

hash() {


@@ 869,7 911,7 @@ hash_siphash() {
	else
		gen_srcs hash::siphash siphash.ha +test.ha
		gen_ssa hash::siphash hash io endian crypto::math \
			fmt strio strings
			fmt memio strings
	fi
}



@@ 1064,8 1106,8 @@ net_ip() {
		gensrcs_net_ip \
			test+test.ha
	fi
	gen_ssa -plinux net::ip bytes endian io strconv strings strio fmt
	gen_ssa -pfreebsd net::ip bytes endian io strconv strings strio fmt
	gen_ssa -plinux net::ip bytes endian io strconv strings memio fmt
	gen_ssa -pfreebsd net::ip bytes endian io strconv strings memio fmt
}

net_tcp() {


@@ 1136,7 1178,7 @@ net_uri() {
			+test.ha
	fi
	gen_ssa net::uri \
		ascii encoding::utf8 fmt io net::ip strconv strings strio
		ascii encoding::utf8 fmt io net::ip strconv strings memio
}

gensrcs_math_complex() {


@@ 1231,11 1273,11 @@ regex() {
	if [ $testing -eq 0 ]; then
		gen_srcs regex regex.ha
		gen_ssa regex ascii bufio encoding::utf8 errors io strconv \
			strings bufio types
			strings bufio types memio
	else
		gen_srcs regex regex.ha +test.ha
		gen_ssa regex encoding::utf8 errors strconv strings fmt io os \
			bufio types
			bufio types memio
	fi
}



@@ 1268,7 1310,7 @@ shlex() {
		gensrcs_shlex \
			+test.ha
	fi
	gen_ssa shlex ascii encoding::utf8 io strings strio
	gen_ssa shlex ascii encoding::utf8 io strings memio
}

gensrcs_sort() {


@@ 1325,24 1367,24 @@ strings() {
strings_template() {
	gen_srcs strings::template \
		template.ha
	gen_ssa strings::template ascii errors fmt io strings strio
	gen_ssa strings::template ascii errors fmt io strings memio
}

strio() {
	gen_srcs strio \
memio() {
	gen_srcs memio \
		stream.ha \
		ops.ha
	gen_ssa strio errors io strings encoding::utf8
	gen_ssa memio errors io strings encoding::utf8
}

temp() {
	gen_srcs -plinux temp +linux.ha
	gen_ssa -plinux temp \
		crypto::random encoding::hex errors fs io os path strio fmt
		crypto::random encoding::hex errors fs io os path memio fmt

	gen_srcs -pfreebsd temp +freebsd.ha
	gen_ssa -pfreebsd temp \
		crypto::random encoding::hex errors fs io os path strio fmt
		crypto::random encoding::hex errors fs io os path memio fmt
}

time() {


@@ 1404,7 1446,7 @@ time_date() {
		tarithm.ha \
		virtual.ha
	gen_ssa -plinux time::date \
		ascii errors fmt io strconv strings strio time time::chrono
		ascii errors fmt io strconv strings memio time time::chrono
	gen_srcs -pfreebsd time::date \
		date.ha \
		daydate.ha \


@@ 1420,7 1462,7 @@ time_date() {
		tarithm.ha \
		virtual.ha
	gen_ssa -pfreebsd time::date \
		ascii errors fmt io strconv strings strio time time::chrono
		ascii errors fmt io strconv strings memio time time::chrono
}

types() {


@@ 1468,27 1510,27 @@ unix_hosts() {
    			+linux.ha \
    			hosts.ha
    		gen_ssa -plinux unix::hosts bufio encoding::utf8 fs io \
    			net::ip os strings
    			net::ip os strings memio

    		gen_srcs -pfreebsd unix::hosts \
    			+freebsd.ha \
    			hosts.ha
    		gen_ssa -pfreebsd unix::hosts bufio encoding::utf8 fs io \
    			net::ip os strings
    			net::ip os strings memio
	else
    		gen_srcs -plinux unix::hosts \
    			+linux.ha \
    			test+test.ha \
    			hosts.ha
    		gen_ssa -plinux unix::hosts bufio encoding::utf8 fs io \
    			net::ip os strings
    			net::ip os strings memio

    		gen_srcs -pfreebsd unix::hosts \
    			+freebsd.ha \
    			test+test.ha \
    			hosts.ha
    		gen_ssa -pfreebsd unix::hosts bufio encoding::utf8 fs io \
    			net::ip os strings
    			net::ip os strings memio
	fi
}



@@ 1497,7 1539,7 @@ unix_passwd() {
		group.ha \
		passwd.ha \
		types.ha
	gen_ssa unix::passwd bufio io os strconv strings
	gen_ssa unix::passwd bufio io os strconv strings memio
}

unix_poll() {


@@ 1512,12 1554,12 @@ unix_resolvconf() {
	gen_srcs -plinux unix::resolvconf \
		+linux.ha \
		load.ha
	gen_ssa -plinux unix::resolvconf os io bufio net::ip strings
	gen_ssa -plinux unix::resolvconf os io bufio memio net::ip strings

	gen_srcs -pfreebsd unix::resolvconf \
		+freebsd.ha \
		load.ha
	gen_ssa -pfreebsd unix::resolvconf os io bufio net::ip strings
	gen_ssa -pfreebsd unix::resolvconf os io bufio memio net::ip strings
}

unix_signal() {


@@ 1557,7 1599,7 @@ unix_tty() {
uuid() {
	gen_srcs uuid \
		uuid.ha
	gen_ssa uuid crypto::random strio fmt endian io bytes bufio strings strconv
	gen_ssa uuid crypto::random fmt endian io bytes memio strings strconv
}

# List of modules and their supported platforms. Place a tab between the module


@@ 1631,6 1673,7 @@ math
math::checked
math::complex
math::random
memio
net			linux freebsd
net::dial
net::dns


@@ 1648,7 1691,6 @@ sort
strconv
strings
strings::template
strio
temp			linux freebsd
test
time			linux freebsd

M scripts/install-mods => scripts/install-mods +1 -1
@@ 19,6 19,7 @@ io
linux
log
math
memio
mime
net
os


@@ 29,7 30,6 @@ shlex
sort
strconv
strings
strio
temp
test
time

M shlex/+test.ha => shlex/+test.ha +5 -5
@@ 2,8 2,8 @@
// (c) 2021 Alexey Yerin <yyp@disroot.org>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use io;
use memio;
use strings;
use strio;

@test fn split() void = {
	const s = split(`hello\ world`)!;


@@ 64,14 64,14 @@ use strio;
	assert(split(`unterminated\ backslash \`) is syntaxerr);
};

fn testquote(sink: *strio::stream, s: str, expected: str) void = {
fn testquote(sink: *memio::stream, s: str, expected: str) void = {
	assert(quote(sink, s)! == len(expected));
	assert(strio::string(sink) == expected);
	strio::reset(sink);
	assert(memio::string(sink)! == expected);
	memio::reset(sink);
};

@test fn quote() void = {
	const sink = strio::dynamic();
	const sink = memio::dynamic();
	defer io::close(&sink)!;
	testquote(&sink, `hello`, `hello`);
	testquote(&sink, `hello world`, `'hello world'`);

M shlex/escape.ha => shlex/escape.ha +3 -3
@@ 4,8 4,8 @@
use ascii;
use encoding::utf8;
use io;
use memio;
use strings;
use strio;

fn is_safe(s: str) bool = {
	const iter = strings::iter(s);


@@ 64,7 64,7 @@ export fn quote(sink: io::handle, s: str) (size | io::error) = {
// Quotes a shell string and returns a new string. The caller must free the
// return value.
export fn quotestr(s: str) str = {
	const sink = strio::dynamic();
	const sink = memio::dynamic();
	quote(&sink, s)!;
	return strio::string(&sink);
	return memio::string(&sink)!;
};

M shlex/split.ha => shlex/split.ha +9 -9
@@ 2,8 2,8 @@
// (c) 2021-2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use io;
use memio;
use strings;
use strio;

// Invalid shell syntax.
export type syntaxerr = !void;


@@ 13,7 13,7 @@ export type syntaxerr = !void;
export fn split(in: const str) ([]str | syntaxerr) = {
	let iter = strings::iter(in);

	let s = strio::dynamic();
	let s = memio::dynamic();
	let slice: []str = [];
	let first = true;
	let dirty = false;


@@ 39,8 39,8 @@ export fn split(in: const str) ([]str | syntaxerr) = {
				break;
			};
			if (!first) {
				append(slice, strio::string(&s));
				s = strio::dynamic();
				append(slice, memio::string(&s)!);
				s = memio::dynamic();
			};
			dirty = false;
		case '\\' =>


@@ 50,7 50,7 @@ export fn split(in: const str) ([]str | syntaxerr) = {
		case '\'' =>
			scan_single(&s, &iter)?;
		case =>
			strio::appendrune(&s, r)!;
			memio::appendrune(&s, r)!;
		};

		if (first) {


@@ 59,7 59,7 @@ export fn split(in: const str) ([]str | syntaxerr) = {
	};

	if (dirty) {
		append(slice, strio::string(&s));
		append(slice, memio::string(&s)!);
	};

	return slice;


@@ 81,7 81,7 @@ fn scan_backslash(out: io::handle, in: *strings::iterator) (void | syntaxerr) = 
		return;
	};

	strio::appendrune(out, r)!;
	memio::appendrune(out, r)!;
};

fn scan_double(out: io::handle, in: *strings::iterator) (void | syntaxerr) = {


@@ 99,7 99,7 @@ fn scan_double(out: io::handle, in: *strings::iterator) (void | syntaxerr) = {
		case '\\' =>
			scan_backslash(out, in)?;
		case =>
			strio::appendrune(out, r)!;
			memio::appendrune(out, r)!;
		};
	};
};


@@ 116,6 116,6 @@ fn scan_single(out: io::handle, in: *strings::iterator) (void | syntaxerr) = {
		if (r == '\'') {
			break;
		};
		strio::appendrune(out, r)!;
		memio::appendrune(out, r)!;
	};
};

M stdlib.mk => stdlib.mk +116 -116
@@ 610,6 610,13 @@ stdlib_deps_any += $(stdlib_math_random_any)
stdlib_math_random_linux = $(stdlib_math_random_any)
stdlib_math_random_freebsd = $(stdlib_math_random_any)

# gen_lib memio (any)
stdlib_memio_any = $(HARECACHE)/memio/memio-any.o
stdlib_env += HARE_TD_memio=$(HARECACHE)/memio/memio.td
stdlib_deps_any += $(stdlib_memio_any)
stdlib_memio_linux = $(stdlib_memio_any)
stdlib_memio_freebsd = $(stdlib_memio_any)

# gen_lib net (linux)
stdlib_net_linux = $(HARECACHE)/net/net-linux.o
stdlib_env += HARE_TD_net=$(HARECACHE)/net/net.td


@@ 750,13 757,6 @@ stdlib_deps_any += $(stdlib_strings_template_any)
stdlib_strings_template_linux = $(stdlib_strings_template_any)
stdlib_strings_template_freebsd = $(stdlib_strings_template_any)

# gen_lib strio (any)
stdlib_strio_any = $(HARECACHE)/strio/strio-any.o
stdlib_env += HARE_TD_strio=$(HARECACHE)/strio/strio.td
stdlib_deps_any += $(stdlib_strio_any)
stdlib_strio_linux = $(stdlib_strio_any)
stdlib_strio_freebsd = $(stdlib_strio_any)

# gen_lib temp (linux)
stdlib_temp_linux = $(HARECACHE)/temp/temp-linux.o
stdlib_env += HARE_TD_temp=$(HARECACHE)/temp/temp.td


@@ 906,11 906,10 @@ $(HARECACHE)/ascii/ascii-any.ssa: $(stdlib_ascii_any_srcs) $(stdlib_rt) $(stdlib

# bufio (+any)
stdlib_bufio_any_srcs = \
	$(STDLIB)/bufio/buffered.ha \
	$(STDLIB)/bufio/memstream.ha \
	$(STDLIB)/bufio/stream.ha \
	$(STDLIB)/bufio/scanner.ha

$(HARECACHE)/bufio/bufio-any.ssa: $(stdlib_bufio_any_srcs) $(stdlib_rt) $(stdlib_io_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
$(HARECACHE)/bufio/bufio-any.ssa: $(stdlib_bufio_any_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/bufio
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nbufio \


@@ 938,7 937,7 @@ stdlib_crypto_any_srcs = \
	$(STDLIB)/crypto/authenc.ha \
	$(STDLIB)/crypto/keyderiv.ha

$(HARECACHE)/crypto/crypto-any.ssa: $(stdlib_crypto_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_argon2_$(PLATFORM)) $(stdlib_crypto_chachapoly_$(PLATFORM)) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM))
$(HARECACHE)/crypto/crypto-any.ssa: $(stdlib_crypto_any_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_argon2_$(PLATFORM)) $(stdlib_crypto_chachapoly_$(PLATFORM)) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/crypto
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto \


@@ 970,7 969,7 @@ $(HARECACHE)/crypto/aes/xts/crypto_aes_xts-any.ssa: $(stdlib_crypto_aes_xts_any_
stdlib_crypto_argon2_any_srcs = \
	$(STDLIB)/crypto/argon2/argon2.ha

$(HARECACHE)/crypto/argon2/crypto_argon2-any.ssa: $(stdlib_crypto_argon2_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_blake2b_$(PLATFORM)) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_rt_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
$(HARECACHE)/crypto/argon2/crypto_argon2-any.ssa: $(stdlib_crypto_argon2_any_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_blake2b_$(PLATFORM)) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_rt_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/crypto/argon2
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::argon2 \


@@ 981,7 980,7 @@ stdlib_crypto_bcrypt_any_srcs = \
	$(STDLIB)/crypto/bcrypt/bcrypt.ha \
	$(STDLIB)/crypto/bcrypt/base64.ha

$(HARECACHE)/crypto/bcrypt/crypto_bcrypt-any.ssa: $(stdlib_crypto_bcrypt_any_srcs) $(stdlib_rt) $(stdlib_crypto_blowfish_$(PLATFORM)) $(stdlib_encoding_base64_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_crypto_$(PLATFORM)) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_crypto_cipher_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM))
$(HARECACHE)/crypto/bcrypt/crypto_bcrypt-any.ssa: $(stdlib_crypto_bcrypt_any_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_$(PLATFORM)) $(stdlib_crypto_blowfish_$(PLATFORM)) $(stdlib_crypto_cipher_$(PLATFORM)) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_encoding_base64_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/crypto/bcrypt
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::bcrypt \


@@ 1130,7 1129,7 @@ stdlib_crypto_rsa_any_srcs = \
	$(STDLIB)/crypto/rsa/keys.ha \
	$(STDLIB)/crypto/rsa/pkcs1.ha

$(HARECACHE)/crypto/rsa/crypto_rsa-any.ssa: $(stdlib_crypto_rsa_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_bigint_$(PLATFORM)) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_crypto_sha1_$(PLATFORM)) $(stdlib_crypto_sha256_$(PLATFORM)) $(stdlib_crypto_sha512_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
$(HARECACHE)/crypto/rsa/crypto_rsa-any.ssa: $(stdlib_crypto_rsa_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_crypto_bigint_$(PLATFORM)) $(stdlib_crypto_math_$(PLATFORM)) $(stdlib_crypto_sha1_$(PLATFORM)) $(stdlib_crypto_sha256_$(PLATFORM)) $(stdlib_crypto_sha512_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/crypto/rsa
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ncrypto::rsa \


@@ 1231,7 1230,7 @@ $(HARECACHE)/dirs/dirs-any.ssa: $(stdlib_dirs_any_srcs) $(stdlib_rt) $(stdlib_er
stdlib_encoding_base64_any_srcs = \
	$(STDLIB)/encoding/base64/base64.ha

$(HARECACHE)/encoding/base64/encoding_base64-any.ssa: $(stdlib_encoding_base64_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/encoding/base64/encoding_base64-any.ssa: $(stdlib_encoding_base64_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/encoding/base64
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nencoding::base64 \


@@ 1241,7 1240,7 @@ $(HARECACHE)/encoding/base64/encoding_base64-any.ssa: $(stdlib_encoding_base64_a
stdlib_encoding_base32_any_srcs = \
	$(STDLIB)/encoding/base32/base32.ha

$(HARECACHE)/encoding/base32/encoding_base32-any.ssa: $(stdlib_encoding_base32_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_os_$(PLATFORM))
$(HARECACHE)/encoding/base32/encoding_base32-any.ssa: $(stdlib_encoding_base32_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/encoding/base32
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nencoding::base32 \


@@ 1251,7 1250,7 @@ $(HARECACHE)/encoding/base32/encoding_base32-any.ssa: $(stdlib_encoding_base32_a
stdlib_encoding_hex_any_srcs = \
	$(STDLIB)/encoding/hex/hex.ha

$(HARECACHE)/encoding/hex/encoding_hex-any.ssa: $(stdlib_encoding_hex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/encoding/hex/encoding_hex-any.ssa: $(stdlib_encoding_hex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/encoding/hex
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nencoding::hex \


@@ 1261,7 1260,7 @@ $(HARECACHE)/encoding/hex/encoding_hex-any.ssa: $(stdlib_encoding_hex_any_srcs) 
stdlib_encoding_pem_any_srcs = \
	$(STDLIB)/encoding/pem/pem.ha

$(HARECACHE)/encoding/pem/encoding_pem-any.ssa: $(stdlib_encoding_pem_any_srcs) $(stdlib_rt) $(stdlib_strings_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_encoding_base64_$(PLATFORM)) $(stdlib_ascii_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
$(HARECACHE)/encoding/pem/encoding_pem-any.ssa: $(stdlib_encoding_pem_any_srcs) $(stdlib_rt) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_encoding_base64_$(PLATFORM)) $(stdlib_ascii_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/encoding/pem
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nencoding::pem \


@@ 1311,7 1310,7 @@ $(HARECACHE)/errors/errors-any.ssa: $(stdlib_errors_any_srcs) $(stdlib_rt) $(std
stdlib_fmt_any_srcs = \
	$(STDLIB)/fmt/fmt.ha

$(HARECACHE)/fmt/fmt-any.ssa: $(stdlib_fmt_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
$(HARECACHE)/fmt/fmt-any.ssa: $(stdlib_fmt_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/fmt
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nfmt \


@@ 1344,7 1343,7 @@ stdlib_format_ini_any_srcs = \
	$(STDLIB)/format/ini/scan.ha \
	$(STDLIB)/format/ini/types.ha

$(HARECACHE)/format/ini/format_ini-any.ssa: $(stdlib_format_ini_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/format/ini/format_ini-any.ssa: $(stdlib_format_ini_any_srcs) $(stdlib_rt) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/format/ini
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nformat::ini \


@@ 1355,7 1354,7 @@ stdlib_format_tar_any_srcs = \
	$(STDLIB)/format/tar/types.ha \
	$(STDLIB)/format/tar/reader.ha

$(HARECACHE)/format/tar/format_tar-any.ssa: $(stdlib_format_tar_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_types_c_$(PLATFORM))
$(HARECACHE)/format/tar/format_tar-any.ssa: $(stdlib_format_tar_any_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_types_c_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/format/tar
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nformat::tar \


@@ 1387,7 1386,7 @@ $(HARECACHE)/getopt/getopt-any.ssa: $(stdlib_getopt_any_srcs) $(stdlib_rt) $(std
stdlib_glob_any_srcs = \
	$(STDLIB)/glob/glob.ha

$(HARECACHE)/glob/glob-any.ssa: $(stdlib_glob_any_srcs) $(stdlib_rt) $(stdlib_fnmatch_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM))
$(HARECACHE)/glob/glob-any.ssa: $(stdlib_glob_any_srcs) $(stdlib_rt) $(stdlib_fnmatch_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/glob
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nglob \


@@ 1413,7 1412,7 @@ stdlib_hare_lex_any_srcs = \
	$(STDLIB)/hare/lex/token.ha \
	$(STDLIB)/hare/lex/lex.ha

$(HARECACHE)/hare/lex/hare_lex-any.ssa: $(stdlib_hare_lex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
$(HARECACHE)/hare/lex/hare_lex-any.ssa: $(stdlib_hare_lex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/hare/lex
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::lex \


@@ 1427,7 1426,7 @@ stdlib_hare_module_any_srcs = \
	$(STDLIB)/hare/module/manifest.ha \
	$(STDLIB)/hare/module/walk.ha

$(HARECACHE)/hare/module/hare_module-any.ssa: $(stdlib_hare_module_any_srcs) $(stdlib_rt) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM)) $(stdlib_hare_parse_$(PLATFORM)) $(stdlib_hare_unparse_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_crypto_sha256_$(PLATFORM)) $(stdlib_dirs_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_ascii_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_temp_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
$(HARECACHE)/hare/module/hare_module-any.ssa: $(stdlib_hare_module_any_srcs) $(stdlib_rt) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM)) $(stdlib_hare_parse_$(PLATFORM)) $(stdlib_hare_unparse_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_crypto_sha256_$(PLATFORM)) $(stdlib_dirs_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_ascii_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_temp_$(PLATFORM)) $(stdlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/hare/module
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::module \


@@ 1460,7 1459,7 @@ stdlib_hare_types_any_srcs = \
	$(STDLIB)/hare/types/store.ha \
	$(STDLIB)/hare/types/types.ha

$(HARECACHE)/hare/types/hare_types-any.ssa: $(stdlib_hare_types_any_srcs) $(stdlib_rt) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_hash_fnv_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
$(HARECACHE)/hare/types/hare_types-any.ssa: $(stdlib_hare_types_any_srcs) $(stdlib_rt) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_hash_fnv_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/hare/types
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::types \


@@ 1477,7 1476,7 @@ stdlib_hare_unit_any_srcs = \
	$(STDLIB)/hare/unit/scope.ha \
	$(STDLIB)/hare/unit/unit.ha

$(HARECACHE)/hare/unit/hare_unit-any.ssa: $(stdlib_hare_unit_any_srcs) $(stdlib_rt) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_types_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_hash_fnv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM))
$(HARECACHE)/hare/unit/hare_unit-any.ssa: $(stdlib_hare_unit_any_srcs) $(stdlib_rt) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_types_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_hash_fnv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/hare/unit
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::unit \


@@ 1493,7 1492,7 @@ stdlib_hare_unparse_any_srcs = \
	$(STDLIB)/hare/unparse/unit.ha \
	$(STDLIB)/hare/unparse/util.ha

$(HARECACHE)/hare/unparse/hare_unparse-any.ssa: $(stdlib_hare_unparse_any_srcs) $(stdlib_rt) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM))
$(HARECACHE)/hare/unparse/hare_unparse-any.ssa: $(stdlib_hare_unparse_any_srcs) $(stdlib_rt) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/hare/unparse
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::unparse \


@@ 1732,6 1731,17 @@ $(HARECACHE)/math/random/math_random-any.ssa: $(stdlib_math_random_any_srcs) $(s
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nmath::random \
		-t$(HARECACHE)/math/random/math_random.td $(stdlib_math_random_any_srcs)

# memio (+any)
stdlib_memio_any_srcs = \
	$(STDLIB)/memio/stream.ha \
	$(STDLIB)/memio/ops.ha

$(HARECACHE)/memio/memio-any.ssa: $(stdlib_memio_any_srcs) $(stdlib_rt) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/memio
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nmemio \
		-t$(HARECACHE)/memio/memio.td $(stdlib_memio_any_srcs)

# net (+linux)
stdlib_net_linux_srcs = \
	$(STDLIB)/net/+linux.ha \


@@ 1796,13 1806,13 @@ stdlib_net_ip_freebsd_srcs = \