~sircmpwn/hare-dev

hare: strio,bufio: merge memstream implementation into memio v7 SUPERSEDED

Autumn!: 1
 strio,bufio: merge memstream implementation into memio

 86 files changed, 1161 insertions(+), 1261 deletions(-)
#1004145 alpine.yml success
#1004146 freebsd.yml success
I think "new()" is the most suitable name, similar to date::new().
I think hare::lex::init() should be renamed to hare::lex::new() or
hare::lex::newlexer(). "init" is reserved for functions that take
pointers and initialize values.
This part is bad. Like the theoretical "stream()" example, name clashes
are inevitable. This should be changed to:
yeah, i've noticed this problem as well. not sure new() is necessarily
the right word to use, since (at least to me) it implies allocation.
maybe we should have init() be for both callee-allocated and
caller-allocated stack initialization? i do agree that we should change
this though, once we've settled on the right conventions would you mind
sending patches for both stdlib and style guide?
functions makes sense. i'm gona send a v8 with s/new/init/g

Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/hare-dev/patches/41754/mbox | git am -3
Learn more about email & git

[PATCH hare v7] strio,bufio: merge memstream implementation into memio Export this patch

- 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>
---
also fixed a bug in dynamic_write
 bufio/README                          |  21 +-
 bufio/scanner.ha                      | 118 ----------
 bufio/scanner_test+test.ha            | 113 +++++++++
 bufio/{buffered.ha => stream.ha}      | 191 ++++-----------
 bufio/stream_test+test.ha             | 117 ++++++++++
 cmd/hare/schedule.ha                  |   1 -
 cmd/harec/context.ha                  |   4 +-
 cmd/harec/gen.ha                      |   8 +-
 cmd/harec/main.ha                     |   2 +-
 cmd/haredoc/docstr.ha                 |  30 +--
 cmd/haredoc/hare.ha                   |   1 -
 cmd/haredoc/html.ha                   |  25 +-
 cmd/haredoc/main.ha                   |   4 +-
 cmd/haredoc/tty.ha                    |  14 +-
 cmd/haredoc/util.ha                   |   8 +-
 crypto/aes/+test/gcm.ha               |  10 +-
 crypto/aes/ctr+test.ha                |  24 +-
 crypto/argon2/argon2.ha               |   4 +-
 crypto/authenc.ha                     |   8 +-
 crypto/bcrypt/base64.ha               |   6 +-
 crypto/bcrypt/bcrypt.ha               |   6 +-
 crypto/blake2b/+test.ha               |  12 +-
 crypto/chacha/+test.ha                |  10 +-
 crypto/chachapoly/chachapoly.ha       |   4 +-
 crypto/chachapoly/encryption+test.ha  |  22 +-
 crypto/rsa/keys.ha                    |  10 +-
 crypto/salsa/+test.ha                 |  10 +-
 encoding/base32/base32.ha             |  26 +--
 encoding/base64/base64.ha             |  26 +--
 encoding/hex/hex.ha                   |  21 +-
 encoding/pem/+test.ha                 |  21 +-
 encoding/pem/pem.ha                   |  20 +-
 fmt/fmt.ha                            |  14 +-
 format/ini/+test.ha                   |  14 +-
 format/tar/reader.ha                  |  19 +-
 glob/glob.ha                          |  74 +++---
 hare/lex/+test.ha                     |  20 +-
 hare/lex/lex.ha                       |  26 +--
 hare/module/context.ha                |   6 +-
 hare/module/manifest.ha               |   4 +-
 hare/module/scan.ha                   |  10 +-
 hare/parse/+test/ident_test.ha        |  12 +-
 hare/parse/+test/loc.ha               |   8 +-
 hare/parse/+test/roundtrip.ha         |   9 +-
 hare/parse/+test/unit_test.ha         |   4 +-
 hare/parse/ident.ha                   |   5 +-
 hare/parse/parse.ha                   |   6 +-
 hare/types/+test.ha                   |   6 +-
 hare/unit/+test.ha                    |   4 +-
 hare/unparse/decl.ha                  |   6 +-
 hare/unparse/ident.ha                 |   6 +-
 hare/unparse/import.ha                |   6 +-
 hare/unparse/type.ha                  |  18 +-
 hash/siphash/+test.ha                 |   1 -
 memio/README                          |  11 +
 {strio => memio}/ops.ha               |  24 +-
 bufio/memstream.ha => memio/stream.ha | 325 ++++++++++++--------------
 mime/system.ha                        |   2 +-
 net/ip/ip.ha                          |   6 +-
 net/uri/fmt.ha                        |   6 +-
 net/uri/parse.ha                      |  44 ++--
 net/uri/query.ha                      |  22 +-
 os/+freebsd/stdfd.ha                  |   8 +-
 os/+linux/stdfd.ha                    |   8 +-
 regex/regex.ha                        |   7 +-
 scripts/gen-stdlib                    | 293 +++++++++++++----------
 scripts/install-mods                  |   2 +-
 shlex/+test.ha                        |  10 +-
 shlex/escape.ha                       |   6 +-
 shlex/split.ha                        |  18 +-
 stdlib.mk                             | 232 +++++++++---------
 strings/template/template.ha          |  30 +--
 strio/README                          |   5 -
 strio/stream.ha                       | 104 ---------
 temp/+freebsd.ha                      |   6 +-
 temp/+linux.ha                        |   6 +-
 test/+test.ha                         |  26 +--
 time/chrono/timezone.ha               |   2 +-
 time/chrono/tzdb.ha                   |   2 +-
 time/date/format.ha                   |  12 +-
 time/date/parse.ha                    |   1 -
 unix/hosts/lookup.ha                  |   3 +-
 unix/passwd/group.ha                  |   5 +-
 unix/passwd/passwd.ha                 |   5 +-
 unix/resolvconf/load.ha               |   3 +-
 uuid/uuid.ha                          |  13 +-
 86 files changed, 1161 insertions(+), 1261 deletions(-)
 create mode 100644 bufio/scanner_test+test.ha
 rename bufio/{buffered.ha => stream.ha} (50%)
 create mode 100644 bufio/stream_test+test.ha
 create mode 100644 memio/README
 rename {strio => memio}/ops.ha (86%)
 rename bufio/memstream.ha => memio/stream.ha (51%)
 delete mode 100644 strio/README
 delete mode 100644 strio/stream.ha

diff --git a/bufio/README b/bufio/README
index cede46e4..8b5943b7 100644
--- a/bufio/README
@@ -1,21 +1,8 @@
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.

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
The [[stream]] implementation 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
@@ -25,6 +12,6 @@ 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]].
diff --git a/bufio/scanner.ha b/bufio/scanner.ha
index 7fe988b1..b52a3885 100644
--- a/bufio/scanner.ha
@@ -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.
@@ -334,111 +324,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();
		};
	};
};
diff --git a/bufio/scanner_test+test.ha b/bufio/scanner_test+test.ha
new file mode 100644
index 00000000..e4bb5e30
--- /dev/null
@@ -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();
		};
	};
};
diff --git a/bufio/buffered.ha b/bufio/stream.ha
similarity index 50%
rename from bufio/buffered.ha
rename to bufio/stream.ha
index a08a12b6..e16b37e5 100644
--- a/bufio/buffered.ha
@@ -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::new(source, rbuf, wbuf);
export fn new(
	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 bufio::new with 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 to bufio::new");
		};

	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);
};
diff --git a/bufio/stream_test+test.ha b/bufio/stream_test+test.ha
new file mode 100644
index 00000000..5d980123
--- /dev/null
@@ -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 = new(&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 = new(&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 = new(&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 = new(&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 = new(&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 = new(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 = new(&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);
};
diff --git a/cmd/hare/schedule.ha b/cmd/hare/schedule.ha
index f0656605..bdda090c 100644
--- a/cmd/hare/schedule.ha
+++ b/cmd/hare/schedule.ha
@@ -16,7 +16,6 @@ use os;
use path;
use shlex;
use strings;
use strio;

fn getenv(var: str) []str = {
	match (os::getenv(var)) {
diff --git a/cmd/harec/context.ha b/cmd/harec/context.ha
index 665df7fa..30002db7 100644
--- a/cmd/harec/context.ha
+++ b/cmd/harec/context.ha
@@ -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 {
diff --git a/cmd/harec/gen.ha b/cmd/harec/gen.ha
index 89ff60ee..2efd3739 100644
--- a/cmd/harec/gen.ha
+++ b/cmd/harec/gen.ha
@@ -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")!;
};

diff --git a/cmd/harec/main.ha b/cmd/harec/main.ha
index 6cc262f4..40dca9bb 100644
--- a/cmd/harec/main.ha
+++ b/cmd/harec/main.ha
@@ -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::new(input, buf, []);
		defer io::close(&bufin)!;

		let lexer = lex::init(&bufin, cmd.args[i]);
diff --git a/cmd/haredoc/docstr.ha b/cmd/haredoc/docstr.ha
index e31b8dfa..5d70e42e 100644
--- a/cmd/haredoc/docstr.ha
+++ b/cmd/haredoc/docstr.ha
@@ -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::new(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;
diff --git a/cmd/haredoc/hare.ha b/cmd/haredoc/hare.ha
index e1a9cf4f..150167d6 100644
--- a/cmd/haredoc/hare.ha
+++ b/cmd/haredoc/hare.ha
@@ -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) = {
diff --git a/cmd/haredoc/html.ha b/cmd/haredoc/html.ha
index b22225f2..94d01b62 100644
--- a/cmd/haredoc/html.ha
+++ b/cmd/haredoc/html.ha
@@ -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";
diff --git a/cmd/haredoc/main.ha b/cmd/haredoc/main.ha
index d09cf576..05e1b826 100644
--- a/cmd/haredoc/main.ha
+++ b/cmd/haredoc/main.ha
@@ -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
diff --git a/cmd/haredoc/tty.ha b/cmd/haredoc/tty.ha
index c5a2eb74..5f981579 100644
--- a/cmd/haredoc/tty.ha
+++ b/cmd/haredoc/tty.ha
@@ -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;
	};

diff --git a/cmd/haredoc/util.ha b/cmd/haredoc/util.ha
index ba17bab0..c5db4065 100644
--- a/cmd/haredoc/util.ha
+++ b/cmd/haredoc/util.ha
@@ -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) = {
diff --git a/crypto/aes/+test/gcm.ha b/crypto/aes/+test/gcm.ha
index 9e2d1bdc..9e644549 100644
--- a/crypto/aes/+test/gcm.ha
+++ b/crypto/aes/+test/gcm.ha
@@ -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)!;
diff --git a/crypto/aes/ctr+test.ha b/crypto/aes/ctr+test.ha
index c8c9631b..2c9282c5 100644
--- a/crypto/aes/ctr+test.ha
+++ b/crypto/aes/ctr+test.ha
@@ -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...];

diff --git a/crypto/argon2/argon2.ha b/crypto/argon2/argon2.ha
index 04f8e3c8..c3516156 100644
--- a/crypto/argon2/argon2.ha
+++ b/crypto/argon2/argon2.ha
@@ -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);
diff --git a/crypto/authenc.ha b/crypto/authenc.ha
index 5a9ad299..9b2c7611 100644
--- a/crypto/authenc.ha
+++ b/crypto/authenc.ha
@@ -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;
diff --git a/crypto/bcrypt/base64.ha b/crypto/bcrypt/base64.ha
index f3c1624a..f9e2276b 100644
--- a/crypto/bcrypt/base64.ha
+++ b/crypto/bcrypt/base64.ha
@@ -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];
diff --git a/crypto/bcrypt/bcrypt.ha b/crypto/bcrypt/bcrypt.ha
index f3ac2acd..30680f9d 100644
--- a/crypto/bcrypt/bcrypt.ha
+++ b/crypto/bcrypt/bcrypt.ha
@@ -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(
diff --git a/crypto/blake2b/+test.ha b/crypto/blake2b/+test.ha
index 8709f62e..6a670c43 100644
--- a/crypto/blake2b/+test.ha
+++ b/crypto/blake2b/+test.ha
@@ -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();
		};
	};
diff --git a/crypto/chacha/+test.ha b/crypto/chacha/+test.ha
index 95ad7382..0a66bb1f 100644
--- a/crypto/chacha/+test.ha
+++ b/crypto/chacha/+test.ha
@@ -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)!;
diff --git a/crypto/chachapoly/chachapoly.ha b/crypto/chachapoly/chachapoly.ha
index a3f62885..88708739 100644
--- a/crypto/chachapoly/chachapoly.ha
+++ b/crypto/chachapoly/chachapoly.ha
@@ -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[..])!;

diff --git a/crypto/chachapoly/encryption+test.ha b/crypto/chachapoly/encryption+test.ha
index 897dde77..458afca2 100644
--- a/crypto/chachapoly/encryption+test.ha
+++ b/crypto/chachapoly/encryption+test.ha
@@ -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)!;
diff --git a/crypto/rsa/keys.ha b/crypto/rsa/keys.ha
index a0e5ec89..bc4ac8b4 100644
--- a/crypto/rsa/keys.ha
+++ b/crypto/rsa/keys.ha
@@ -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)!;
diff --git a/crypto/salsa/+test.ha b/crypto/salsa/+test.ha
index 7aab6c42..7cd30ad2 100644
--- a/crypto/salsa/+test.ha
+++ b/crypto/salsa/+test.ha
@@ -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)!;
diff --git a/encoding/base32/base32.ha b/encoding/base32/base32.ha
index c136372a..51ee2441 100644
--- a/encoding/base32/base32.ha
+++ b/encoding/base32/base32.ha
@@ -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;
diff --git a/encoding/base64/base64.ha b/encoding/base64/base64.ha
index d8d25099..193d2e34 100644
--- a/encoding/base64/base64.ha
+++ b/encoding/base64/base64.ha
@@ -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])));

@@ -415,15 +415,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);
	};
};

@@ -463,7 +463,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 = [];
@@ -485,7 +485,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 = [];
@@ -516,7 +516,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;
diff --git a/encoding/hex/hex.ha b/encoding/hex/hex.ha
index 5402402c..5b9df046 100644
--- a/encoding/hex/hex.ha
+++ b/encoding/hex/hex.ha
@@ -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");
diff --git a/encoding/pem/+test.ha b/encoding/pem/+test.ha
index 3ac0ceb3..dd7f1a39 100644
--- a/encoding/pem/+test.ha
+++ b/encoding/pem/+test.ha
@@ -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);

diff --git a/encoding/pem/pem.ha b/encoding/pem/pem.ha
index 9b832485..a4b8de19 100644
--- a/encoding/pem/pem.ha
+++ b/encoding/pem/pem.ha
@@ -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::new(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 {
diff --git a/fmt/fmt.ha b/fmt/fmt.ha
index 360197a4..6882af10 100644
--- a/fmt/fmt.ha
+++ b/fmt/fmt.ha
@@ -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]);
};
diff --git a/format/ini/+test.ha b/format/ini/+test.ha
index caa21d4e..23b8e02d 100644
--- a/format/ini/+test.ha
+++ b/format/ini/+test.ha
@@ -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);

diff --git a/format/tar/reader.ha b/format/tar/reader.ha
index 688d5eaa..031950fb 100644
--- a/format/tar/reader.ha
+++ b/format/tar/reader.ha
@@ -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 =>
diff --git a/glob/glob.ha b/glob/glob.ha
index c527027c..ca28b9b2 100644
--- a/glob/glob.ha
+++ b/glob/glob.ha
@@ -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)!,
	);
diff --git a/hare/lex/+test.ha b/hare/lex/+test.ha
index 92204b05..635616a1 100644
--- a/hare/lex/+test.ha
+++ b/hare/lex/+test.ha
@@ -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,
diff --git a/hare/lex/lex.ha b/hare/lex/lex.ha
index 30203461..12af8d32 100644
--- a/hare/lex/lex.ha
+++ b/hare/lex/lex.ha
@@ -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)!;
};

diff --git a/hare/module/context.ha b/hare/module/context.ha
index ac39ffbc..938212ab 100644
--- a/hare/module/context.ha
+++ b/hare/module/context.ha
@@ -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 = {
diff --git a/hare/module/manifest.ha b/hare/module/manifest.ha
index a886f5f2..527aa784 100644
--- a/hare/module/manifest.ha
+++ b/hare/module/manifest.ha
@@ -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::new(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::new(truefile, [], wbuf);
	defer {
		bufio::flush(file)!;
		fs::remove(ctx.fs, name): void;
diff --git a/hare/module/scan.ha b/hare/module/scan.ha
index 89543be8..b90a2856 100644
--- a/hare/module/scan.ha
+++ b/hare/module/scan.ha
@@ -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::new(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;
diff --git a/hare/parse/+test/ident_test.ha b/hare/parse/+test/ident_test.ha
index 98ec9d8e..ffcc272f 100644
--- a/hare/parse/+test/ident_test.ha
+++ b/hare/parse/+test/ident_test.ha
@@ -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;
diff --git a/hare/parse/+test/loc.ha b/hare/parse/+test/loc.ha
index fb16b5ef..289de4fd 100644
--- a/hare/parse/+test/loc.ha
+++ b/hare/parse/+test/loc.ha
@@ -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)) {
diff --git a/hare/parse/+test/roundtrip.ha b/hare/parse/+test/roundtrip.ha
index d1328f87..da7fc8d3 100644
--- a/hare/parse/+test/roundtrip.ha
+++ b/hare/parse/+test/roundtrip.ha
@@ -3,18 +3,17 @@
// (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 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 {
@@ -28,10 +27,10 @@ fn roundtrip(src: str) void = {
		},
	};
	defer ast::subunit_finish(u);
	let out = strio::dynamic();
	let out = memio::dynamic();
	defer io::close(&out)!;
	let z = unparse::subunit(&out, u) as size;
	let unsrc = strio::string(&out);
	let unsrc = memio::string(&out)!;
	assert(z == len(unsrc));
	if (unsrc != src) {
		fmt::errorfln("=== wanted\n{}", src)!;
diff --git a/hare/parse/+test/unit_test.ha b/hare/parse/+test/unit_test.ha
index 8c9adb6c..5fefc965 100644
--- a/hare/parse/+test/unit_test.ha
+++ b/hare/parse/+test/unit_test.ha
@@ -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)!;
diff --git a/hare/parse/ident.ha b/hare/parse/ident.ha
index b4e5e8bf..762b36a6 100644
--- a/hare/parse/ident.ha
+++ b/hare/parse/ident.ha
@@ -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);
	return ident(&lexer);
diff --git a/hare/parse/parse.ha b/hare/parse/parse.ha
index a0515872..18a6e823 100644
--- a/hare/parse/parse.ha
+++ b/hare/parse/parse.ha
@@ -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
diff --git a/hare/types/+test.ha b/hare/types/+test.ha
index 1c9c5134..9c81eebc 100644
--- a/hare/types/+test.ha
+++ b/hare/types/+test.ha
@@ -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)!;
diff --git a/hare/unit/+test.ha b/hare/unit/+test.ha
index 6c763d16..045fa2e3 100644
--- a/hare/unit/+test.ha
+++ b/hare/unit/+test.ha
@@ -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)!);
diff --git a/hare/unparse/decl.ha b/hare/unparse/decl.ha
index 6c777c8d..b33d150e 100644
--- a/hare/unparse/decl.ha
+++ b/hare/unparse/decl.ha
@@ -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;
};
diff --git a/hare/unparse/ident.ha b/hare/unparse/ident.ha
index 8ec64e09..8438b8aa 100644
--- a/hare/unparse/ident.ha
+++ b/hare/unparse/ident.ha
@@ -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 = {
diff --git a/hare/unparse/import.ha b/hare/unparse/import.ha
index 72b620ff..983bf2cf 100644
--- a/hare/unparse/import.ha
+++ b/hare/unparse/import.ha
@@ -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);
	};
diff --git a/hare/unparse/type.ha b/hare/unparse/type.ha
index e4704db0..1aa41ed1 100644
--- a/hare/unparse/type.ha
+++ b/hare/unparse/type.ha
@@ -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;
	};

@@ -356,9 +356,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)!;
diff --git a/hash/siphash/+test.ha b/hash/siphash/+test.ha
index 19b21fb2..0e3fba46 100644
--- a/hash/siphash/+test.ha
+++ b/hash/siphash/+test.ha
@@ -2,7 +2,6 @@ use endian;
use fmt;
use hash;
use io;
use strio;
use strings;

@test fn siphash() void = {
diff --git a/memio/README b/memio/README
new file mode 100644
index 00000000..da9720d2
--- /dev/null
+++ b/memio/README
@@ -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.
diff --git a/strio/ops.ha b/memio/ops.ha
similarity index 86%
rename from strio/ops.ha
rename to memio/ops.ha
index 28275282..cc71b662 100644
--- a/strio/ops.ha
+++ b/memio/ops.ha
@@ -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);
	};
};

diff --git a/bufio/memstream.ha b/memio/stream.ha
similarity index 51%
rename from bufio/memstream.ha
rename to memio/stream.ha
index c06868ab..3a56cc90 100644
--- a/bufio/memstream.ha
+++ b/memio/stream.ha
@@ -4,143 +4,83 @@
// (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;
use errors;

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

const memstream_vt_r: io::vtable = io::vtable {
const fixed_vt: 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 {
const dynamic_vt: 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 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. 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);
// 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 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;
// 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 the current buffer of a [[fixed]] or [[dynamic]] stream.
export fn buffer(in: *memstream) []u8 = {
	return in.buf;
// 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];
};

// 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];
// 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]);
};

// Truncates the dynamic buffer, freeing memory associated with it and setting
// its length to zero.
export fn truncate(in: *memstream) (void | errors::unsupported) = {
// 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;
	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) = {
export fn borrowedread(st: *stream, amt: size) ([]u8 | io::EOF) = {
	if (len(st.buf) - st.pos < amt) {
		return io::EOF;
	};
@@ -149,20 +89,8 @@ export fn borrowedread(st: *memstream, amt: size) ([]u8 | io::EOF) = {
	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;
	let s = s: *stream;
	if (len(s.buf) == s.pos && len(buf) != 0) {
		return io::EOF;
	};
@@ -182,37 +110,108 @@ fn seek(
	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;
	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 || src.writer == null) {
	if (src.reader != &read || dest.writer == null) {
		return errors::unsupported;
	};
	let src = src: *memstream;
	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(io::mode::RDWR);
	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);
@@ -228,72 +227,48 @@ fn copy(dest: *io::stream, src: *io::stream) (size | io::error) = {
	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);
	assert(io::close(&s) is void);

	let sl: []u8 = alloc([1, 2, 3]);
	let s = dynamic_from(sl, io::mode::WRITE);
	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 == 5: io::off);
	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, 1, 2, 3, 4, 5, 6]));
	assert(io::read(&s, buf[..]) as io::error is errors::unsupported);
	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, io::mode::READ);
	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::write(&s, [1, 2]) as io::error is errors::unsupported);
	assert(io::writeall(&s, [3, 4]) as size == 2 && bytes::equal(buffer(&s), [1, 2, 3, 4]));
	io::close(&s)!;
	assert(io::writeall(&s, [1, 2]) as io::error is errors::unsupported);
	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, io::mode::READ);
	let sink = dynamic(io::mode::WRITE);
	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, io::mode::READ);
	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, io::mode::READ);
	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, io::mode::READ);
	let source = dynamic_from(in);
	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);
	let stream = dynamic();
	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);
	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");
};
diff --git a/mime/system.ha b/mime/system.ha
index b3208963..6ae4f09e 100644
--- a/mime/system.ha
+++ b/mime/system.ha
@@ -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::new(file, buf, []);

	for (true) {
		const line = match (bufio::scanline(&strm)) {
diff --git a/net/ip/ip.ha b/net/ip/ip.ha
index 153d27b7..400a2f10 100644
--- a/net/ip/ip.ha
+++ b/net/ip/ip.ha
@@ -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) = {
diff --git a/net/uri/fmt.ha b/net/uri/fmt.ha
index b4986463..866ae377 100644
--- a/net/uri/fmt.ha
+++ b/net/uri/fmt.ha
@@ -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
@@ -108,7 +108,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)!;
};
diff --git a/net/uri/parse.ha b/net/uri/parse.ha
index 8650c3b6..563c8376 100644
--- a/net/uri/parse.ha
+++ b/net/uri/parse.ha
@@ -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;
				};
diff --git a/net/uri/query.ha b/net/uri/query.ha
index 8c9dd0e4..0e752fa3 100644
--- a/net/uri/query.ha
+++ b/net/uri/query.ha
@@ -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 = {
diff --git a/os/+freebsd/stdfd.ha b/os/+freebsd/stdfd.ha
index 48dd069a..3bc2a3a1 100644
--- a/os/+freebsd/stdfd.ha
+++ b/os/+freebsd/stdfd.ha
@@ -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::new(stdin_file, stdinbuf, []);
	stdin = &stdin_bufio;

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

diff --git a/os/+linux/stdfd.ha b/os/+linux/stdfd.ha
index 48dd069a..3bc2a3a1 100644
--- a/os/+linux/stdfd.ha
+++ b/os/+linux/stdfd.ha
@@ -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::new(stdin_file, stdinbuf, []);
	stdin = &stdin_bufio;

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

diff --git a/regex/regex.ha b/regex/regex.ha
index 4c25023a..b1573c25 100644
--- a/regex/regex.ha
+++ b/regex/regex.ha
@@ -5,6 +5,7 @@ use bufio;
use encoding::utf8;
use errors;
use io;
use memio;
use strconv;
use strings;

@@ -772,7 +773,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;
};

@@ -781,7 +782,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;
@@ -797,7 +798,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)) {
diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib
index cfb51097..adf4a512 100755
--- a/scripts/gen-stdlib
+++ b/scripts/gen-stdlib
@@ -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
}

@@ -1059,8 +1101,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() {
@@ -1131,7 +1173,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() {
@@ -1226,10 +1268,11 @@ regex() {
	if [ $testing -eq 0 ]; then
		gen_srcs regex regex.ha
		gen_ssa regex ascii bufio encoding::utf8 errors io strconv \
			strings bufio
			strings bufio memio
	else
		gen_srcs regex regex.ha +test.ha
		gen_ssa regex encoding::utf8 errors strconv strings fmt io os bufio
		gen_ssa regex encoding::utf8 errors strconv strings fmt io memio \
			os bufio
	fi
}

@@ -1262,7 +1305,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
}

slices() {
@@ -1329,24 +1372,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 slices encoding::utf8
	gen_ssa memio errors io strings slices 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() {
@@ -1408,7 +1451,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 \
@@ -1424,7 +1467,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() {
@@ -1469,12 +1512,12 @@ unix_hosts() {
	gen_srcs -plinux unix::hosts \
		+linux.ha \
		lookup.ha
	gen_ssa -plinux unix::hosts os io bufio net::ip strings
	gen_ssa -plinux unix::hosts os io bufio net::ip strings memio

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

unix_passwd() {
@@ -1482,7 +1525,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() {
@@ -1497,12 +1540,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() {
@@ -1542,7 +1585,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
@@ -1615,6 +1658,7 @@ log	linux freebsd
math
math::complex
math::random
memio
net			linux freebsd
net::dial
net::dns
@@ -1633,7 +1677,6 @@ sort
strconv
strings
strings::template
strio
temp			linux freebsd
test
time			linux freebsd
diff --git a/scripts/install-mods b/scripts/install-mods
index 84dcb439..c5a06213 100755
--- a/scripts/install-mods
+++ b/scripts/install-mods
@@ -19,6 +19,7 @@ io
linux
log
math
memio
mime
net
os
@@ -30,7 +31,6 @@ slices
sort
strconv
strings
strio
temp
test
time
diff --git a/shlex/+test.ha b/shlex/+test.ha
index 9baf2fbd..02b6242f 100644
--- a/shlex/+test.ha
+++ b/shlex/+test.ha
@@ -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'`);
diff --git a/shlex/escape.ha b/shlex/escape.ha
index 1048fc7c..05365852 100644
--- a/shlex/escape.ha
+++ b/shlex/escape.ha
@@ -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)!;
};
diff --git a/shlex/split.ha b/shlex/split.ha
index a8781467..33734b17 100644
--- a/shlex/split.ha
+++ b/shlex/split.ha
@@ -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)!;
	};
};
diff --git a/stdlib.mk b/stdlib.mk
index db858e39..6eb3d6b8 100644
--- a/stdlib.mk
+++ b/stdlib.mk
@@ -603,6 +603,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 \
@@ -1722,6 +1721,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_slices_$(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 \
@@ -1786,13 +1796,13 @@ stdlib_net_ip_freebsd_srcs = \
	$(STDLIB)/net/ip/+freebsd.ha \
	$(STDLIB)/net/ip/ip.ha

$(HARECACHE)/net/ip/net_ip-linux.ssa: $(stdlib_net_ip_linux_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
$(HARECACHE)/net/ip/net_ip-linux.ssa: $(stdlib_net_ip_linux_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/net/ip
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nnet::ip \
		-t$(HARECACHE)/net/ip/net_ip.td $(stdlib_net_ip_linux_srcs)

$(HARECACHE)/net/ip/net_ip-freebsd.ssa: $(stdlib_net_ip_freebsd_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
$(HARECACHE)/net/ip/net_ip-freebsd.ssa: $(stdlib_net_ip_freebsd_srcs) $(stdlib_rt) $(stdlib_bytes_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/net/ip
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nnet::ip \
@@ -1883,7 +1893,7 @@ stdlib_net_uri_any_srcs = \
	$(STDLIB)/net/uri/query.ha \
	$(STDLIB)/net/uri/uri.ha

$(HARECACHE)/net/uri/net_uri-any.ssa: $(stdlib_net_uri_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM))
$(HARECACHE)/net/uri/net_uri-any.ssa: $(stdlib_net_uri_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/net/uri
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nnet::uri \
@@ -1971,7 +1981,7 @@ $(HARECACHE)/path/path-any.ssa: $(stdlib_path_any_srcs) $(stdlib_rt) $(stdlib_st
stdlib_regex_any_srcs = \
	$(STDLIB)/regex/regex.ha

$(HARECACHE)/regex/regex-any.ssa: $(stdlib_regex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM))
$(HARECACHE)/regex/regex-any.ssa: $(stdlib_regex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/regex
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nregex \
@@ -1982,7 +1992,7 @@ stdlib_shlex_any_srcs = \
	$(STDLIB)/shlex/escape.ha \
	$(STDLIB)/shlex/split.ha

$(HARECACHE)/shlex/shlex-any.ssa: $(stdlib_shlex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM))
$(HARECACHE)/shlex/shlex-any.ssa: $(stdlib_shlex_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/shlex
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nshlex \
@@ -2060,28 +2070,17 @@ $(HARECACHE)/strings/strings-any.ssa: $(stdlib_strings_any_srcs) $(stdlib_rt) $(
stdlib_strings_template_any_srcs = \
	$(STDLIB)/strings/template/template.ha

$(HARECACHE)/strings/template/strings_template-any.ssa: $(stdlib_strings_template_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM))
$(HARECACHE)/strings/template/strings_template-any.ssa: $(stdlib_strings_template_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/strings/template
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nstrings::template \
		-t$(HARECACHE)/strings/template/strings_template.td $(stdlib_strings_template_any_srcs)

# strio (+any)
stdlib_strio_any_srcs = \
	$(STDLIB)/strio/stream.ha \
	$(STDLIB)/strio/ops.ha

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

# temp (+linux)
stdlib_temp_linux_srcs = \
	$(STDLIB)/temp/+linux.ha

$(HARECACHE)/temp/temp-linux.ssa: $(stdlib_temp_linux_srcs) $(stdlib_rt) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_path_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
$(HARECACHE)/temp/temp-linux.ssa: $(stdlib_temp_linux_srcs) $(stdlib_rt) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_path_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/temp
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ntemp \
@@ -2091,7 +2090,7 @@ $(HARECACHE)/temp/temp-linux.ssa: $(stdlib_temp_linux_srcs) $(stdlib_rt) $(stdli
stdlib_temp_freebsd_srcs = \
	$(STDLIB)/temp/+freebsd.ha

$(HARECACHE)/temp/temp-freebsd.ssa: $(stdlib_temp_freebsd_srcs) $(stdlib_rt) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_path_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
$(HARECACHE)/temp/temp-freebsd.ssa: $(stdlib_temp_freebsd_srcs) $(stdlib_rt) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_path_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/temp
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ntemp \
@@ -2184,7 +2183,7 @@ stdlib_time_date_linux_srcs = \
	$(STDLIB)/time/date/tarithm.ha \
	$(STDLIB)/time/date/virtual.ha

$(HARECACHE)/time/date/time_date-linux.ssa: $(stdlib_time_date_linux_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_time_chrono_$(PLATFORM))
$(HARECACHE)/time/date/time_date-linux.ssa: $(stdlib_time_date_linux_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_time_chrono_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/time/date
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ntime::date \
@@ -2206,7 +2205,7 @@ stdlib_time_date_freebsd_srcs = \
	$(STDLIB)/time/date/tarithm.ha \
	$(STDLIB)/time/date/virtual.ha

$(HARECACHE)/time/date/time_date-freebsd.ssa: $(stdlib_time_date_freebsd_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_time_chrono_$(PLATFORM))
$(HARECACHE)/time/date/time_date-freebsd.ssa: $(stdlib_time_date_freebsd_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_time_chrono_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/time/date
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ntime::date \
@@ -2271,7 +2270,7 @@ stdlib_unix_hosts_linux_srcs = \
	$(STDLIB)/unix/hosts/+linux.ha \
	$(STDLIB)/unix/hosts/lookup.ha

$(HARECACHE)/unix/hosts/unix_hosts-linux.ssa: $(stdlib_unix_hosts_linux_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/unix/hosts/unix_hosts-linux.ssa: $(stdlib_unix_hosts_linux_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/unix/hosts
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nunix::hosts \
@@ -2282,7 +2281,7 @@ stdlib_unix_hosts_freebsd_srcs = \
	$(STDLIB)/unix/hosts/+freebsd.ha \
	$(STDLIB)/unix/hosts/lookup.ha

$(HARECACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(stdlib_unix_hosts_freebsd_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(stdlib_unix_hosts_freebsd_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/unix/hosts
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nunix::hosts \
@@ -2294,7 +2293,7 @@ stdlib_unix_passwd_any_srcs = \
	$(STDLIB)/unix/passwd/passwd.ha \
	$(STDLIB)/unix/passwd/types.ha

$(HARECACHE)/unix/passwd/unix_passwd-any.ssa: $(stdlib_unix_passwd_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/unix/passwd/unix_passwd-any.ssa: $(stdlib_unix_passwd_any_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/unix/passwd
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nunix::passwd \
@@ -2327,7 +2326,7 @@ stdlib_unix_resolvconf_linux_srcs = \
	$(STDLIB)/unix/resolvconf/+linux.ha \
	$(STDLIB)/unix/resolvconf/load.ha

$(HARECACHE)/unix/resolvconf/unix_resolvconf-linux.ssa: $(stdlib_unix_resolvconf_linux_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/unix/resolvconf/unix_resolvconf-linux.ssa: $(stdlib_unix_resolvconf_linux_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/unix/resolvconf
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nunix::resolvconf \
@@ -2338,7 +2337,7 @@ stdlib_unix_resolvconf_freebsd_srcs = \
	$(STDLIB)/unix/resolvconf/+freebsd.ha \
	$(STDLIB)/unix/resolvconf/load.ha

$(HARECACHE)/unix/resolvconf/unix_resolvconf-freebsd.ssa: $(stdlib_unix_resolvconf_freebsd_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
$(HARECACHE)/unix/resolvconf/unix_resolvconf-freebsd.ssa: $(stdlib_unix_resolvconf_freebsd_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/unix/resolvconf
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nunix::resolvconf \
@@ -2401,7 +2400,7 @@ $(HARECACHE)/unix/tty/unix_tty-freebsd.ssa: $(stdlib_unix_tty_freebsd_srcs) $(st
stdlib_uuid_any_srcs = \
	$(STDLIB)/uuid/uuid.ha

$(HARECACHE)/uuid/uuid-any.ssa: $(stdlib_uuid_any_srcs) $(stdlib_rt) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_strio_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM))
$(HARECACHE)/uuid/uuid-any.ssa: $(stdlib_uuid_any_srcs) $(stdlib_rt) $(stdlib_crypto_random_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_endian_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(HARECACHE)/uuid
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nuuid \
@@ -3011,6 +3010,13 @@ testlib_deps_any += $(testlib_math_random_any)
testlib_math_random_linux = $(testlib_math_random_any)
testlib_math_random_freebsd = $(testlib_math_random_any)

# gen_lib memio (any)
testlib_memio_any = $(TESTCACHE)/memio/memio-any.o
testlib_env += HARE_TD_memio=$(TESTCACHE)/memio/memio.td
testlib_deps_any += $(testlib_memio_any)
testlib_memio_linux = $(testlib_memio_any)
testlib_memio_freebsd = $(testlib_memio_any)

# gen_lib net (linux)
testlib_net_linux = $(TESTCACHE)/net/net-linux.o
testlib_env += HARE_TD_net=$(TESTCACHE)/net/net.td
@@ -3158,13 +3164,6 @@ testlib_deps_any += $(testlib_strings_template_any)
testlib_strings_template_linux = $(testlib_strings_template_any)
testlib_strings_template_freebsd = $(testlib_strings_template_any)

# gen_lib strio (any)
testlib_strio_any = $(TESTCACHE)/strio/strio-any.o
testlib_env += HARE_TD_strio=$(TESTCACHE)/strio/strio.td
testlib_deps_any += $(testlib_strio_any)
testlib_strio_linux = $(testlib_strio_any)
testlib_strio_freebsd = $(testlib_strio_any)

# gen_lib temp (linux)
testlib_temp_linux = $(TESTCACHE)/temp/temp-linux.o
testlib_env += HARE_TD_temp=$(TESTCACHE)/temp/temp.td
@@ -3314,11 +3313,12 @@ $(TESTCACHE)/ascii/ascii-any.ssa: $(testlib_ascii_any_srcs) $(testlib_rt) $(test

# bufio (+any)
testlib_bufio_any_srcs = \
	$(STDLIB)/bufio/buffered.ha \
	$(STDLIB)/bufio/memstream.ha \
	$(STDLIB)/bufio/scanner.ha
	$(STDLIB)/bufio/stream.ha \
	$(STDLIB)/bufio/scanner.ha \
	$(STDLIB)/bufio/stream_test+test.ha \
	$(STDLIB)/bufio/scanner_test+test.ha

$(TESTCACHE)/bufio/bufio-any.ssa: $(testlib_bufio_any_srcs) $(testlib_rt) $(testlib_io_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_types_$(PLATFORM))
$(TESTCACHE)/bufio/bufio-any.ssa: $(testlib_bufio_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/bufio
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nbufio \
@@ -3347,7 +3347,7 @@ testlib_crypto_any_srcs = \
	$(STDLIB)/crypto/keyderiv.ha \
	$(STDLIB)/crypto/+test/authenc_test.ha

$(TESTCACHE)/crypto/crypto-any.ssa: $(testlib_crypto_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_argon2_$(PLATFORM)) $(testlib_crypto_chachapoly_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM))
$(TESTCACHE)/crypto/crypto-any.ssa: $(testlib_crypto_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_argon2_$(PLATFORM)) $(testlib_crypto_chachapoly_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto \
@@ -3364,7 +3364,7 @@ testlib_crypto_aes_any_srcs = \
	$(STDLIB)/crypto/aes/rt+test.ha \
	$(STDLIB)/crypto/aes/+test/gcm.ha

$(TESTCACHE)/crypto/aes/crypto_aes-any.ssa: $(testlib_crypto_aes_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_rt_$(PLATFORM))
$(TESTCACHE)/crypto/aes/crypto_aes-any.ssa: $(testlib_crypto_aes_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_rt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto/aes
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::aes \
@@ -3386,7 +3386,7 @@ testlib_crypto_argon2_any_srcs = \
	$(STDLIB)/crypto/argon2/argon2.ha \
	$(STDLIB)/crypto/argon2/+test.ha

$(TESTCACHE)/crypto/argon2/crypto_argon2-any.ssa: $(testlib_crypto_argon2_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_blake2b_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_rt_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_types_$(PLATFORM))
$(TESTCACHE)/crypto/argon2/crypto_argon2-any.ssa: $(testlib_crypto_argon2_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_blake2b_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_rt_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto/argon2
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::argon2 \
@@ -3398,7 +3398,7 @@ testlib_crypto_bcrypt_any_srcs = \
	$(STDLIB)/crypto/bcrypt/base64.ha \
	$(STDLIB)/crypto/bcrypt/+test.ha

$(TESTCACHE)/crypto/bcrypt/crypto_bcrypt-any.ssa: $(testlib_crypto_bcrypt_any_srcs) $(testlib_rt) $(testlib_crypto_blowfish_$(PLATFORM)) $(testlib_encoding_base64_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_crypto_$(PLATFORM)) $(testlib_crypto_random_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_strconv_$(PLATFORM))
$(TESTCACHE)/crypto/bcrypt/crypto_bcrypt-any.ssa: $(testlib_crypto_bcrypt_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_$(PLATFORM)) $(testlib_crypto_blowfish_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_crypto_random_$(PLATFORM)) $(testlib_encoding_base64_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto/bcrypt
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::bcrypt \
@@ -3410,7 +3410,7 @@ testlib_crypto_blake2b_any_srcs = \
	$(STDLIB)/crypto/blake2b/+test.ha \
	$(STDLIB)/crypto/blake2b/vectors+test.ha

$(TESTCACHE)/crypto/blake2b/crypto_blake2b-any.ssa: $(testlib_crypto_blake2b_any_srcs) $(testlib_rt) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_bytes_$(PLATFORM))
$(TESTCACHE)/crypto/blake2b/crypto_blake2b-any.ssa: $(testlib_crypto_blake2b_any_srcs) $(testlib_rt) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_bytes_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto/blake2b
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::blake2b \
@@ -3451,7 +3451,7 @@ testlib_crypto_chacha_any_srcs = \
	$(STDLIB)/crypto/chacha/chacha20.ha \
	$(STDLIB)/crypto/chacha/+test.ha

$(TESTCACHE)/crypto/chacha/crypto_chacha-any.ssa: $(testlib_crypto_chacha_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM))
$(TESTCACHE)/crypto/chacha/crypto_chacha-any.ssa: $(testlib_crypto_chacha_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto/chacha
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::chacha \
@@ -3561,7 +3561,7 @@ testlib_crypto_rsa_any_srcs = \
	$(STDLIB)/crypto/rsa/+test/keys_test.ha \
	$(STDLIB)/crypto/rsa/+test/pkcs1_test.ha

$(TESTCACHE)/crypto/rsa/crypto_rsa-any.ssa: $(testlib_crypto_rsa_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_bigint_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_crypto_sha1_$(PLATFORM)) $(testlib_crypto_sha256_$(PLATFORM)) $(testlib_crypto_sha512_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_types_$(PLATFORM))
$(TESTCACHE)/crypto/rsa/crypto_rsa-any.ssa: $(testlib_crypto_rsa_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_bigint_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_crypto_sha1_$(PLATFORM)) $(testlib_crypto_sha256_$(PLATFORM)) $(testlib_crypto_sha512_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto/rsa
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::rsa \
@@ -3583,7 +3583,7 @@ testlib_crypto_salsa_any_srcs = \
	$(STDLIB)/crypto/salsa/salsa20.ha \
	$(STDLIB)/crypto/salsa/+test.ha

$(TESTCACHE)/crypto/salsa/crypto_salsa-any.ssa: $(testlib_crypto_salsa_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_types_$(PLATFORM)) $(testlib_io_$(PLATFORM))
$(TESTCACHE)/crypto/salsa/crypto_salsa-any.ssa: $(testlib_crypto_salsa_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_crypto_cipher_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/crypto/salsa
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncrypto::salsa \
@@ -3670,7 +3670,7 @@ $(TESTCACHE)/dirs/dirs-any.ssa: $(testlib_dirs_any_srcs) $(testlib_rt) $(testlib
testlib_encoding_base64_any_srcs = \
	$(STDLIB)/encoding/base64/base64.ha

$(TESTCACHE)/encoding/base64/encoding_base64-any.ssa: $(testlib_encoding_base64_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/encoding/base64/encoding_base64-any.ssa: $(testlib_encoding_base64_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/encoding/base64
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nencoding::base64 \
@@ -3680,7 +3680,7 @@ $(TESTCACHE)/encoding/base64/encoding_base64-any.ssa: $(testlib_encoding_base64_
testlib_encoding_base32_any_srcs = \
	$(STDLIB)/encoding/base32/base32.ha

$(TESTCACHE)/encoding/base32/encoding_base32-any.ssa: $(testlib_encoding_base32_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_os_$(PLATFORM))
$(TESTCACHE)/encoding/base32/encoding_base32-any.ssa: $(testlib_encoding_base32_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/encoding/base32
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nencoding::base32 \
@@ -3690,7 +3690,7 @@ $(TESTCACHE)/encoding/base32/encoding_base32-any.ssa: $(testlib_encoding_base32_
testlib_encoding_hex_any_srcs = \
	$(STDLIB)/encoding/hex/hex.ha

$(TESTCACHE)/encoding/hex/encoding_hex-any.ssa: $(testlib_encoding_hex_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/encoding/hex/encoding_hex-any.ssa: $(testlib_encoding_hex_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/encoding/hex
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nencoding::hex \
@@ -3701,7 +3701,7 @@ testlib_encoding_pem_any_srcs = \
	$(STDLIB)/encoding/pem/pem.ha \
	$(STDLIB)/encoding/pem/+test.ha

$(TESTCACHE)/encoding/pem/encoding_pem-any.ssa: $(testlib_encoding_pem_any_srcs) $(testlib_rt) $(testlib_strings_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_encoding_base64_$(PLATFORM)) $(testlib_ascii_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_bytes_$(PLATFORM))
$(TESTCACHE)/encoding/pem/encoding_pem-any.ssa: $(testlib_encoding_pem_any_srcs) $(testlib_rt) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_encoding_base64_$(PLATFORM)) $(testlib_ascii_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_bytes_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/encoding/pem
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nencoding::pem \
@@ -3751,7 +3751,7 @@ $(TESTCACHE)/errors/errors-any.ssa: $(testlib_errors_any_srcs) $(testlib_rt) $(t
testlib_fmt_any_srcs = \
	$(STDLIB)/fmt/fmt.ha

$(TESTCACHE)/fmt/fmt-any.ssa: $(testlib_fmt_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_types_$(PLATFORM))
$(TESTCACHE)/fmt/fmt-any.ssa: $(testlib_fmt_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_types_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/fmt
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nfmt \
@@ -3786,7 +3786,7 @@ testlib_format_ini_any_srcs = \
	$(STDLIB)/format/ini/types.ha \
	$(STDLIB)/format/ini/+test.ha

$(TESTCACHE)/format/ini/format_ini-any.ssa: $(testlib_format_ini_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/format/ini/format_ini-any.ssa: $(testlib_format_ini_any_srcs) $(testlib_rt) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/format/ini
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nformat::ini \
@@ -3797,7 +3797,7 @@ testlib_format_tar_any_srcs = \
	$(STDLIB)/format/tar/types.ha \
	$(STDLIB)/format/tar/reader.ha

$(TESTCACHE)/format/tar/format_tar-any.ssa: $(testlib_format_tar_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_types_c_$(PLATFORM))
$(TESTCACHE)/format/tar/format_tar-any.ssa: $(testlib_format_tar_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_types_c_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/format/tar
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nformat::tar \
@@ -3830,7 +3830,7 @@ testlib_glob_any_srcs = \
	$(STDLIB)/glob/glob.ha \
	$(STDLIB)/glob/+test.ha

$(TESTCACHE)/glob/glob-any.ssa: $(testlib_glob_any_srcs) $(testlib_rt) $(testlib_fnmatch_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM))
$(TESTCACHE)/glob/glob-any.ssa: $(testlib_glob_any_srcs) $(testlib_rt) $(testlib_fnmatch_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/glob
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nglob \
@@ -3857,7 +3857,7 @@ testlib_hare_lex_any_srcs = \
	$(STDLIB)/hare/lex/lex.ha \
	$(STDLIB)/hare/lex/+test.ha

$(TESTCACHE)/hare/lex/hare_lex-any.ssa: $(testlib_hare_lex_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_path_$(PLATFORM))
$(TESTCACHE)/hare/lex/hare_lex-any.ssa: $(testlib_hare_lex_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/hare/lex
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::lex \
@@ -3871,7 +3871,7 @@ testlib_hare_module_any_srcs = \
	$(STDLIB)/hare/module/manifest.ha \
	$(STDLIB)/hare/module/walk.ha

$(TESTCACHE)/hare/module/hare_module-any.ssa: $(testlib_hare_module_any_srcs) $(testlib_rt) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_crypto_sha256_$(PLATFORM)) $(testlib_dirs_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_ascii_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_temp_$(PLATFORM)) $(testlib_path_$(PLATFORM))
$(TESTCACHE)/hare/module/hare_module-any.ssa: $(testlib_hare_module_any_srcs) $(testlib_rt) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_crypto_sha256_$(PLATFORM)) $(testlib_dirs_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_ascii_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_temp_$(PLATFORM)) $(testlib_path_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/hare/module
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::module \
@@ -3893,7 +3893,7 @@ testlib_hare_parse_any_srcs = \
	$(STDLIB)/hare/parse/+test/types.ha \
	$(STDLIB)/hare/parse/+test/unit_test.ha

$(TESTCACHE)/hare/parse/hare_parse-any.ssa: $(testlib_hare_parse_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_types_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_math_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM))
$(TESTCACHE)/hare/parse/hare_parse-any.ssa: $(testlib_hare_parse_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_types_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_math_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/hare/parse
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::parse \
@@ -3911,7 +3911,7 @@ testlib_hare_types_any_srcs = \
	$(STDLIB)/hare/types/types.ha \
	$(STDLIB)/hare/types/+test.ha

$(TESTCACHE)/hare/types/hare_types-any.ssa: $(testlib_hare_types_any_srcs) $(testlib_rt) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_hash_fnv_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_io_$(PLATFORM))
$(TESTCACHE)/hare/types/hare_types-any.ssa: $(testlib_hare_types_any_srcs) $(testlib_rt) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_hash_fnv_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_io_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/hare/types
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::types \
@@ -3929,7 +3929,7 @@ testlib_hare_unit_any_srcs = \
	$(STDLIB)/hare/unit/unit.ha \
	$(STDLIB)/hare/unit/+test.ha

$(TESTCACHE)/hare/unit/hare_unit-any.ssa: $(testlib_hare_unit_any_srcs) $(testlib_rt) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_types_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_hash_fnv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM))
$(TESTCACHE)/hare/unit/hare_unit-any.ssa: $(testlib_hare_unit_any_srcs) $(testlib_rt) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_types_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_hash_fnv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/hare/unit
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::unit \
@@ -3945,7 +3945,7 @@ testlib_hare_unparse_any_srcs = \
	$(STDLIB)/hare/unparse/unit.ha \
	$(STDLIB)/hare/unparse/util.ha

$(TESTCACHE)/hare/unparse/hare_unparse-any.ssa: $(testlib_hare_unparse_any_srcs) $(testlib_rt) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM))
$(TESTCACHE)/hare/unparse/hare_unparse-any.ssa: $(testlib_hare_unparse_any_srcs) $(testlib_rt) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/hare/unparse
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::unparse \
@@ -4017,7 +4017,7 @@ testlib_hash_siphash_any_srcs = \
	$(STDLIB)/hash/siphash/siphash.ha \
	$(STDLIB)/hash/siphash/+test.ha

$(TESTCACHE)/hash/siphash/hash_siphash-any.ssa: $(testlib_hash_siphash_any_srcs) $(testlib_rt) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/hash/siphash/hash_siphash-any.ssa: $(testlib_hash_siphash_any_srcs) $(testlib_rt) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_crypto_math_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/hash/siphash
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhash::siphash \
@@ -4184,6 +4184,17 @@ $(TESTCACHE)/math/random/math_random-any.ssa: $(testlib_math_random_any_srcs) $(
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nmath::random \
		-t$(TESTCACHE)/math/random/math_random.td $(testlib_math_random_any_srcs)

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

$(TESTCACHE)/memio/memio-any.ssa: $(testlib_memio_any_srcs) $(testlib_rt) $(testlib_errors_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_slices_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/memio
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nmemio \
		-t$(TESTCACHE)/memio/memio.td $(testlib_memio_any_srcs)

# net (+linux)
testlib_net_linux_srcs = \
	$(STDLIB)/net/+linux.ha \
@@ -4250,13 +4261,13 @@ testlib_net_ip_freebsd_srcs = \
	$(STDLIB)/net/ip/ip.ha \
	$(STDLIB)/net/ip/test+test.ha