~sircmpwn/hare-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
5 4

[PATCH hare v4] strio,bufio: merge into memio

Details
Message ID
<20230515233220.14095-1-autumnull@posteo.net>
DKIM signature
pass
Download raw message
Patch: +1094 -1180
Additionally, memio functions will now error instead of aborting.

Signed-off-by: Autumn! <autumnull@posteo.net>
---
fixed freebsd test :)
 bufio/README                          |  17 +-
 bufio/buffered.ha                     | 113 ----------
 bufio/buffered_test+test.ha           | 117 ++++++++++
 bufio/scanner.ha                      | 118 ----------
 bufio/scanner_test+test.ha            | 113 ++++++++++
 cmd/hare/schedule.ha                  |   1 -
 cmd/harec/context.ha                  |   4 +-
 cmd/harec/gen.ha                      |   8 +-
 cmd/haredoc/docstr.ha                 |  26 +--
 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 +-
 config.sh                             |  42 ++++
 crypto/aes/+test/gcm.ha               |  10 +-
 crypto/aes/ctr+test.ha                |  24 +-
 crypto/argon2/argon2.ha               |   4 +-
 crypto/authenc.ha                     |  10 +-
 crypto/bcrypt/base64.ha               |   6 +-
 crypto/bcrypt/bcrypt.ha               |   6 +-
 crypto/blake2b/+test.ha               |  12 +-
 crypto/chacha/+test.ha                |  10 +-
 crypto/rsa/keys.ha                    |  10 +-
 crypto/salsa/+test.ha                 |  10 +-
 datetime/format.ha                    |  12 +-
 datetime/parse.ha                     |   1 -
 encoding/base32/base32.ha             |  26 +--
 encoding/base64/base64.ha             |  26 +--
 encoding/hex/hex.ha                   |  21 +-
 encoding/pem/+test.ha                 |  21 +-
 encoding/pem/pem.ha                   |  12 +-
 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/scan.ha                   |   8 +-
 hare/parse/+test/ident.ha             |  10 +-
 hare/parse/+test/loc.ha               |   8 +-
 hare/parse/+test/roundtrip.ha         |   9 +-
 hare/parse/+test/unit.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                          |  17 ++
 {strio => memio}/ops.ha               |  24 +-
 bufio/memstream.ha => memio/stream.ha | 279 +++++++++++------------
 net/ip/ip.ha                          |   6 +-
 net/uri/fmt.ha                        |   6 +-
 net/uri/parse.ha                      |  44 ++--
 net/uri/query.ha                      |  22 +-
 regex/regex.ha                        |   7 +-
 scripts/gen-stdlib                    | 305 +++++++++++++-------------
 scripts/install-mods                  |   2 +-
 shlex/+test.ha                        |  10 +-
 shlex/escape.ha                       |   6 +-
 shlex/split.ha                        |  18 +-
 stdlib.mk                             | 220 +++++++++----------
 strings/template/template.ha          |  30 +--
 strio/README                          |   5 -
 strio/stream.ha                       | 104 ---------
 temp/+freebsd.ha                      |   6 +-
 temp/+linux.ha                        |   6 +-
 test/+test.ha                         |  26 +--
 unix/hosts/lookup.ha                  |   3 +-
 unix/passwd/group.ha                  |   5 +-
 unix/passwd/passwd.ha                 |   5 +-
 unix/resolvconf/load.ha               |   3 +-
 uuid/uuid.ha                          |  13 +-
 78 files changed, 1094 insertions(+), 1180 deletions(-)
 create mode 100644 bufio/buffered_test+test.ha
 create mode 100644 bufio/scanner_test+test.ha
 create mode 100644 config.sh
 create mode 100644 memio/README
 rename {strio => memio}/ops.ha (86%)
 rename bufio/memstream.ha => memio/stream.ha (60%)
 delete mode 100644 strio/README
 delete mode 100644 strio/stream.ha

diff --git a/bufio/README b/bufio/README
index cede46e4..d8fd5f00 100644
--- a/bufio/README
@@ -2,20 +2,7 @@ bufio provides [[io::stream]] implementations which provide 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 [[buffered]] is used to batch read and write
operations against an underlying stream. The caller may use small, frequent read
and write operations, which bufio will batch into larger, less frequent reads
and writes. The caller must supply either one or two temporary buffers for
@@ -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.
diff --git a/bufio/buffered.ha b/bufio/buffered.ha
index 982f481a..36ba58b6 100644
--- a/bufio/buffered.ha
@@ -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/buffered_test+test.ha b/bufio/buffered_test+test.ha
new file mode 100644
index 00000000..38b7a436
--- /dev/null
@@ -0,0 +1,117 @@
use bytes;
use io;
use memio;
use strings;

@test fn buffered_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 = 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 = memio::fixed(sourcebuf);

	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 = memio::dynamic();
	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(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 = buffered(&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 = buffered(&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 = 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(&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/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/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/haredoc/docstr.ha b/cmd/haredoc/docstr.ha
index e31b8dfa..0b9f396e 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;
@@ -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 a5ff75ab..8ddf2d4d 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 ec64008a..d7aba5f9 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 e21e40a0..cd1fd64f 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/config.sh b/config.sh
new file mode 100644
index 00000000..3a54fa38
--- /dev/null
+++ b/config.sh
@@ -0,0 +1,42 @@
# this file will be source'd by the build script.

# variables used to configure the build
ARCH=${ARCH:-$(uname -m | awk '{print tolower($0)}')}
PLATFORM=${PLATFORM:-$(uname -o | awk '{print tolower($0)}')}
TAGS="$ARCH $PLATFORM"
HAREFLAGS=

# commands used by the build script
AR=ar
AS=as
HAREC=harec
LD=ld
QBE=qbe
SCDOC=scdoc

# build locations
CACHE=.cache
BINOUT=.bin

# install locations
PREFIX="/usr/local"
BINDIR="$PREFIX/bin"
MANDIR="$PREFIX/share/man"
SRCDIR="$PREFIX/src"
STDLIB="$SRCDIR/hare/stdlib"

# variables that will be embedded in the binary with -D definitions
HAREPATH="$SRCDIR/hare/stdlib:$SRCDIR/hare/third-party"
VERSION=$(./scripts/version)
AARCH64_AR=ar
AARCH64_AS=as
AARCH64_CC=cc
AARCH64_LD=ld
RISCV64_AR=ar
RISCV64_AS=as
RISCV64_CC=cc
RISCV64_LD=ld
X86_64_AR=ar
X86_64_AS=as
X86_64_CC=cc
X86_64_LD=ld
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 e7a7e25f..1d18d071 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 1c57564d..807f5875 100644
--- a/crypto/authenc.ha
+++ b/crypto/authenc.ha
@@ -2,7 +2,6 @@
// (c) 2022 Armin Preiml <apreiml@strohwolke.at>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use bytes;
use bufio;
use crypto::chacha;
use crypto::cipher;
use crypto::poly1305;
@@ -11,6 +10,7 @@ use crypto::math;
use endian;
use errors;
use io;
use memio;

// A secret session key.
export type sessionkey = [32]u8;
@@ -86,12 +86,12 @@ export fn encrypt(
	let otk: poly1305::key = [0...];
	defer bytes::zero(otk);

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

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

	chacha::xchacha20_init(&s, &cipherbuf, key, nonce);
	chacha::setctr(&s, 1);
@@ -169,7 +169,7 @@ export fn decrypt(
	let otk: poly1305::key = [0...];
	defer bytes::zero(otk);

	let otkbuf = bufio::fixed(otk, io::mode::WRITE);
	let otkbuf = memio::fixed(otk);
	chacha::xchacha20_init(&s, &otkbuf, key, &box.1);
	io::writeall(&s, otk)!;

@@ -184,7 +184,7 @@ export fn decrypt(
	};

	let plaintext = ciphertext;
	let cipherbuf = bufio::fixed(ciphertext, io::mode::READ);
	let cipherbuf = memio::fixed(ciphertext);

	chacha::xchacha20_init(&s, &cipherbuf, key, &box.1);
	chacha::setctr(&s, 1);
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/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/datetime/format.ha b/datetime/format.ha
index 409b752f..535c99fe 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -6,8 +6,8 @@ use ascii;
use errors;
use fmt;
use io;
use memio;
use strings;
use strio;
use time;
use time::chrono;

@@ -88,17 +88,17 @@ export fn bsformat(
	layout: str,
	dt: *datetime,
) (str | io::error) = {
	let sink = strio::fixed(buf);
	let sink = memio::fixed(buf);
	format(&sink, layout, dt)?;
	return strio::string(&sink);
	return memio::string(&sink)!;
};

// Formats a [[datetime]] and writes it into a heap-allocated string.
// The caller must free the return value.
export fn asformat(layout: str, dt: *datetime) (str | io::error) = {
	let sink = strio::dynamic();
	let sink = memio::dynamic();
	format(&sink, layout, dt)?;
	return strio::string(&sink);
	return memio::string(&sink)!;
};

fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
@@ -226,7 +226,7 @@ export fn format(
			if (r == '%') {
				escaped = true;
			} else {
				strio::appendrune(h, r)?;
				memio::appendrune(h, r)?;
			};
		};
	};
diff --git a/datetime/parse.ha b/datetime/parse.ha
index c509888d..55494977 100644
--- a/datetime/parse.ha
+++ b/datetime/parse.ha
@@ -7,7 +7,6 @@ use errors;
use io;
use strconv;
use strings;
use strio;
use time;
use time::chrono;

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..4a59def8 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 ";
@@ -17,7 +17,7 @@ const suffix: str = "-----";

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

@@ -52,7 +52,7 @@ export fn newdecoder(in: io::handle) decoder = {
	return decoder {
		in = bufio::buffered(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 d5a2113e..9cd18416 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...) (io::error | size) =
// 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...) (io::error | size) =
// 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 2ba73570..3c085d5c 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 flags = 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: flags...) generator = {
	let ss = strstack_init();
	strio::concat(strstack_push(&ss), pattern)!;
	memio::concat(strstack_push(&ss), pattern)!;
	let bs = flags::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 & flags::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 & flags::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 ebad0b0b..9644966f 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>", flags::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 acf8925a..15a4a1ef 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/scan.ha b/hare/module/scan.ha
index 89543be8..714168e5 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;

@@ -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.ha b/hare/parse/+test/ident.ha
index 9ed121f6..1bb37bd8 100644
--- a/hare/parse/+test/ident.ha
+++ b/hare/parse/+test/ident.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 = "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;
@@ -24,7 +24,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;
@@ -37,7 +37,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;
@@ -51,7 +51,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 4c37da2d..77910630 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::flags::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.ha b/hare/parse/+test/unit.ha
index e9915f97..ffa7f0d8 100644
--- a/hare/parse/+test/unit.ha
+++ b/hare/parse/+test/unit.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 ba527a04..812925ce 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) = {
@@ -45,7 +44,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 64b917cb..2af99a15 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"
@@ -48,7 +48,7 @@ fn want(lexer: *lex::lexer, want: lex::ltok...) (lex::token | error) = {
		};
	};
	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 7e39485d..70fae3ef 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 0d6c06d5..93aee603 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 e53670f5..e8d7a2d9 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;
	};

@@ -359,9 +359,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..0b642e4e
--- /dev/null
+++ b/memio/README
@@ -0,0 +1,17 @@
memio provides streams 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
memio-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]] and [[string]]. 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.

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..a04f9dff 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);
		truncate(&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);
		truncate(&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);
		truncate(&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);
		truncate(&st)!;
	};
};

diff --git a/bufio/memstream.ha b/memio/stream.ha
similarity index 60%
rename from bufio/memstream.ha
rename to memio/stream.ha
index 9b34c5bb..4e5e7aa5 100644
--- a/bufio/memstream.ha
+++ b/memio/stream.ha
@@ -4,143 +4,89 @@
// (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 transferred 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;
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 = {
export fn buffer(in: *stream) []u8 = {
	return in.buf;
};

// Returns the current contents of the buffer as a string. The return value
// is borrowed from the stream, and will be freed when the stream is closed.
// Use [[strings::dup]] to extend its lifetime.
export fn string(in: *stream) (str | utf8::invalid) = {
	return strings::fromutf8(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 = {
export fn reset(in: *stream) void = {
	in.pos = 0;
	in.buf = in.buf[..0];
};

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

// Reads data from a [[dynamic]] or [[fixed]] stream and returns a slice
// borrowed from the internal buffer.
export fn borrowedread(st: *memstream, amt: size) ([]u8 | io::EOF) = {
export fn borrowedread(st: *stream, amt: size) ([]u8 | io::EOF) = {
	if (len(st.buf) - st.pos <= amt) {
		return io::EOF;
	};
@@ -149,20 +95,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,21 +116,21 @@ fn seek(
	off: io::off,
	w: io::whence
) (io::off | io::error) = {
	let s = s: *memstream;
	let s = s: *stream;
	switch (w) {
	case io::whence::SET =>
		if (len(s.buf) < off: size) {
			abort("invalid offset");
			return errors::invalid;
		};
		s.pos = off: size;
	case io::whence::CUR =>
		if (s.pos + off: size > len(s.buf)) {
			abort("invalid offset");
			return errors::invalid;
		};
		s.pos += off: size;
	case io::whence::END =>
		if (len(s.buf) - (-off): size < len(s.buf)) {
			abort("invalid offset");
		if (0 < off) {
			return errors::invalid;
		};
		s.pos = len(s.buf) - (-off): size;
	};
@@ -204,15 +138,91 @@ fn seek(
};

fn copy(dest: *io::stream, src: *io::stream) (io::error | size) = {
	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 end = if (spare < len(buf)) spare else len(buf);
	s.buf[s.pos..s.pos+end] = buf[..end];
	if (end < len(buf)) {
		append(s.buf, buf[end..]...);
		s.pos += len(buf[end..]);
	};
	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);
@@ -232,58 +242,35 @@ fn copy(dest: *io::stream, src: *io::stream) (io::error | size) = {
	assert(len(buffer(&s)) == 0);

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

@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/net/ip/ip.ha b/net/ip/ip.ha
index 9c84b3a0..32c7d7bb 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)) (io::error | size) = {
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/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 a272faa3..2353fb72 100755
--- a/scripts/gen-stdlib
+++ b/scripts/gen-stdlib
@@ -162,7 +162,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
}

@@ -177,7 +177,6 @@ ascii() {
bufio() {
	gen_srcs bufio \
		buffered.ha \
		memstream.ha \
		scanner.ha
	gen_ssa bufio io bytes strings encoding::utf8 errors types
}
@@ -195,50 +194,15 @@ bytes() {
	gen_ssa bytes types
}

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 strio \
		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 strio \
		time time::chrono
}

crypto() {
	if [ $testing -eq 0 ]
	then
		gen_srcs crypto \
			authenc.ha \
			keyderiv.ha
		gen_ssa crypto bufio bytes crypto::argon2 crypto::chacha \
		gen_ssa crypto bytes crypto::argon2 crypto::chacha \
			crypto::cihper crypto::poly1305 crypto::mac \
			crypto::math endian errors io
			crypto::math endian errors io memio
	else
		gen_srcs crypto \
			authenc.ha \
@@ -246,7 +210,7 @@ crypto() {
			+test/authenc.ha
		gen_ssa crypto bytes bufio crypto::argon2 crypto::chacha \
			crypto::cihper crypto::poly1305 crypto::mac \
			crypto::math endian errors io
			crypto::math endian errors io memio
	fi
}

@@ -271,8 +235,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
}

@@ -290,13 +254,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
}

@@ -307,9 +271,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() {
@@ -326,7 +290,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
}

@@ -373,7 +337,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
}

@@ -465,7 +429,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() {
@@ -487,8 +451,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
}

@@ -570,6 +534,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
@@ -579,19 +578,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
}

@@ -600,13 +599,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
}
@@ -642,7 +641,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() {
@@ -677,14 +676,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() {
@@ -708,7 +707,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() {
@@ -722,6 +721,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.ha \
			+test/ident.ha \
			+test/loc.ha \
			+test/roundtrip.ha \
			+test/types.ha \
			+test/unit.ha
		gen_ssa hare::parse ascii hare::ast hare::lex hare::unparse \
			io memio fmt types strings math encoding::utf8
	fi
}

gensrcs_hare_types() {
	gen_srcs hare::types \
		'+$(ARCH)/writesize.ha' \
@@ -740,11 +802,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
}

@@ -766,11 +828,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
}

@@ -783,71 +845,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.ha \
			+test/ident.ha \
			+test/loc.ha \
			+test/roundtrip.ha \
			+test/types.ha \
			+test/unit.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() {
@@ -895,7 +893,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
}

@@ -1080,8 +1078,8 @@ net_ip() {
		gensrcs_net_ip \
			+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() {
@@ -1152,7 +1150,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() {
@@ -1243,10 +1241,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
}

@@ -1279,7 +1278,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() {
@@ -1346,24 +1345,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() {
@@ -1451,12 +1450,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() {
@@ -1464,7 +1463,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() {
@@ -1479,12 +1478,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() {
@@ -1524,7 +1523,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
@@ -1597,6 +1596,7 @@ log	linux freebsd
math
math::complex
math::random
memio
net			linux freebsd
net::dial
net::dns
@@ -1615,7 +1615,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 5f9f5756..baebdf46 100755
--- a/scripts/install-mods
+++ b/scripts/install-mods
@@ -20,6 +20,7 @@ io
linux
log
math
memio
mime
net
os
@@ -31,7 +32,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 9ee5cb08..01b30cf5 100644
--- a/stdlib.mk
+++ b/stdlib.mk
@@ -606,6 +606,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
@@ -753,13 +760,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
@@ -900,7 +900,6 @@ $(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/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))
@@ -931,7 +930,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_chacha_$(PLATFORM)) $(stdlib_crypto_cihper_$(PLATFORM)) $(stdlib_crypto_poly1305_$(PLATFORM)) $(stdlib_crypto_mac_$(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_chacha_$(PLATFORM)) $(stdlib_crypto_cihper_$(PLATFORM)) $(stdlib_crypto_poly1305_$(PLATFORM)) $(stdlib_crypto_mac_$(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 \
@@ -963,7 +962,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 \
@@ -974,7 +973,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 \
@@ -1113,7 +1112,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 \
@@ -1216,7 +1215,7 @@ stdlib_datetime_linux_srcs = \
	$(STDLIB)/datetime/timezone.ha \
	$(STDLIB)/datetime/virtual.ha

$(HARECACHE)/datetime/datetime-linux.ssa: $(stdlib_datetime_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)/datetime/datetime-linux.ssa: $(stdlib_datetime_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)/datetime
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ndatetime \
@@ -1238,7 +1237,7 @@ stdlib_datetime_freebsd_srcs = \
	$(STDLIB)/datetime/timezone.ha \
	$(STDLIB)/datetime/virtual.ha

$(HARECACHE)/datetime/datetime-freebsd.ssa: $(stdlib_datetime_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)/datetime/datetime-freebsd.ssa: $(stdlib_datetime_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)/datetime
	@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ndatetime \
@@ -1258,7 +1257,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 \
@@ -1268,7 +1267,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 \
@@ -1278,7 +1277,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 \
@@ -1288,7 +1287,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 \
@@ -1338,7 +1337,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 \
@@ -1371,7 +1370,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 \
@@ -1382,7 +1381,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 \
@@ -1414,7 +1413,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 \
@@ -1440,7 +1439,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 \
@@ -1454,7 +1453,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 \
@@ -1487,7 +1486,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 \
@@ -1504,7 +1503,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 \
@@ -1520,7 +1519,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 \
@@ -1755,6 +1754,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 \
@@ -1817,13 +1827,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 \
@@ -1914,7 +1924,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 \
@@ -1998,7 +2008,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 \
@@ -2009,7 +2019,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 \
@@ -2087,28 +2097,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 \
@@ -2118,7 +2117,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 \
@@ -2254,7 +2253,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 \
@@ -2265,7 +2264,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 \
@@ -2277,7 +2276,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 \
@@ -2310,7 +2309,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 \
@@ -2321,7 +2320,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 \
@@ -2384,7 +2383,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 \
@@ -2999,6 +2998,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
@@ -3146,13 +3152,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
@@ -3293,7 +3292,6 @@ $(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

$(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))
@@ -3325,7 +3323,7 @@ testlib_crypto_any_srcs = \
	$(STDLIB)/crypto/keyderiv.ha \
	$(STDLIB)/crypto/+test/authenc.ha

$(TESTCACHE)/crypto/crypto-any.ssa: $(testlib_crypto_any_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_crypto_argon2_$(PLATFORM)) $(testlib_crypto_chacha_$(PLATFORM)) $(testlib_crypto_cihper_$(PLATFORM)) $(testlib_crypto_poly1305_$(PLATFORM)) $(testlib_crypto_mac_$(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_bufio_$(PLATFORM)) $(testlib_crypto_argon2_$(PLATFORM)) $(testlib_crypto_chacha_$(PLATFORM)) $(testlib_crypto_cihper_$(PLATFORM)) $(testlib_crypto_poly1305_$(PLATFORM)) $(testlib_crypto_mac_$(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 \
@@ -3342,7 +3340,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 \
@@ -3364,7 +3362,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 \
@@ -3376,7 +3374,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 \
@@ -3388,7 +3386,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 \
@@ -3429,7 +3427,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 \
@@ -3528,7 +3526,7 @@ testlib_crypto_rsa_any_srcs = \
	$(STDLIB)/crypto/rsa/+test/keys.ha \
	$(STDLIB)/crypto/rsa/+test/pkcs1.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 \
@@ -3550,7 +3548,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 \
@@ -3639,7 +3637,7 @@ testlib_datetime_linux_srcs = \
	$(STDLIB)/datetime/timezone.ha \
	$(STDLIB)/datetime/virtual.ha

$(TESTCACHE)/datetime/datetime-linux.ssa: $(testlib_datetime_linux_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM))
$(TESTCACHE)/datetime/datetime-linux.ssa: $(testlib_datetime_linux_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/datetime
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ndatetime \
@@ -3661,7 +3659,7 @@ testlib_datetime_freebsd_srcs = \
	$(STDLIB)/datetime/timezone.ha \
	$(STDLIB)/datetime/virtual.ha

$(TESTCACHE)/datetime/datetime-freebsd.ssa: $(testlib_datetime_freebsd_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM))
$(TESTCACHE)/datetime/datetime-freebsd.ssa: $(testlib_datetime_freebsd_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/datetime
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ndatetime \
@@ -3681,7 +3679,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 \
@@ -3691,7 +3689,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 \
@@ -3701,7 +3699,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 \
@@ -3712,7 +3710,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 \
@@ -3762,7 +3760,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 \
@@ -3797,7 +3795,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 \
@@ -3808,7 +3806,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 \
@@ -3841,7 +3839,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 \
@@ -3868,7 +3866,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 \
@@ -3882,7 +3880,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 \
@@ -3904,7 +3902,7 @@ testlib_hare_parse_any_srcs = \
	$(STDLIB)/hare/parse/+test/types.ha \
	$(STDLIB)/hare/parse/+test/unit.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_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(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 \
@@ -3922,7 +3920,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 \
@@ -3940,7 +3938,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 \
@@ -3956,7 +3954,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 \
@@ -4028,7 +4026,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 \
@@ -4201,6 +4199,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 \
@@ -4265,13 +4274,13 @@ testlib_net_ip_freebsd_srcs = \
	$(STDLIB)/net/ip/ip.ha \
	$(STDLIB)/net/ip/+test.ha

$(TESTCACHE)/net/ip/net_ip-linux.ssa: $(testlib_net_ip_linux_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
$(TESTCACHE)/net/ip/net_ip-linux.ssa: $(testlib_net_ip_linux_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/net/ip
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nnet::ip \
		-t$(TESTCACHE)/net/ip/net_ip.td $(testlib_net_ip_linux_srcs)

$(TESTCACHE)/net/ip/net_ip-freebsd.ssa: $(testlib_net_ip_freebsd_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
$(TESTCACHE)/net/ip/net_ip-freebsd.ssa: $(testlib_net_ip_freebsd_srcs) $(testlib_rt) $(testlib_bytes_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/net/ip
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nnet::ip \
@@ -4363,7 +4372,7 @@ testlib_net_uri_any_srcs = \
	$(STDLIB)/net/uri/uri.ha \
	$(STDLIB)/net/uri/+test.ha

$(TESTCACHE)/net/uri/net_uri-any.ssa: $(testlib_net_uri_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM))
$(TESTCACHE)/net/uri/net_uri-any.ssa: $(testlib_net_uri_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/net/uri
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nnet::uri \
@@ -4448,7 +4457,7 @@ testlib_regex_any_srcs = \
	$(STDLIB)/regex/regex.ha \
	$(STDLIB)/regex/+test.ha

$(TESTCACHE)/regex/regex-any.ssa: $(testlib_regex_any_srcs) $(testlib_rt) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_bufio_$(PLATFORM))
$(TESTCACHE)/regex/regex-any.ssa: $(testlib_regex_any_srcs) $(testlib_rt) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_bufio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/regex
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nregex \
@@ -4460,7 +4469,7 @@ testlib_shlex_any_srcs = \
	$(STDLIB)/shlex/split.ha \
	$(STDLIB)/shlex/+test.ha

$(TESTCACHE)/shlex/shlex-any.ssa: $(testlib_shlex_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM))
$(TESTCACHE)/shlex/shlex-any.ssa: $(testlib_shlex_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/shlex
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nshlex \
@@ -4541,28 +4550,17 @@ $(TESTCACHE)/strings/strings-any.ssa: $(testlib_strings_any_srcs) $(testlib_rt)
testlib_strings_template_any_srcs = \
	$(STDLIB)/strings/template/template.ha

$(TESTCACHE)/strings/template/strings_template-any.ssa: $(testlib_strings_template_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM))
$(TESTCACHE)/strings/template/strings_template-any.ssa: $(testlib_strings_template_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/strings/template
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nstrings::template \
		-t$(TESTCACHE)/strings/template/strings_template.td $(testlib_strings_template_any_srcs)

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

$(TESTCACHE)/strio/strio-any.ssa: $(testlib_strio_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)/strio
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nstrio \
		-t$(TESTCACHE)/strio/strio.td $(testlib_strio_any_srcs)

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

$(TESTCACHE)/temp/temp-linux.ssa: $(testlib_temp_linux_srcs) $(testlib_rt) $(testlib_crypto_random_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_path_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
$(TESTCACHE)/temp/temp-linux.ssa: $(testlib_temp_linux_srcs) $(testlib_rt) $(testlib_crypto_random_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_path_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/temp
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ntemp \
@@ -4572,7 +4570,7 @@ $(TESTCACHE)/temp/temp-linux.ssa: $(testlib_temp_linux_srcs) $(testlib_rt) $(tes
testlib_temp_freebsd_srcs = \
	$(STDLIB)/temp/+freebsd.ha

$(TESTCACHE)/temp/temp-freebsd.ssa: $(testlib_temp_freebsd_srcs) $(testlib_rt) $(testlib_crypto_random_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_path_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
$(TESTCACHE)/temp/temp-freebsd.ssa: $(testlib_temp_freebsd_srcs) $(testlib_rt) $(testlib_crypto_random_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_path_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/temp
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ntemp \
@@ -4584,7 +4582,7 @@ testlib_test_any_srcs = \
	$(STDLIB)/test/+test.ha \
	$(STDLIB)/test/fail+test.ha

$(TESTCACHE)/test/test-any.ssa: $(testlib_test_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_fnmatch_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_rt_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_unix_signal_$(PLATFORM))
$(TESTCACHE)/test/test-any.ssa: $(testlib_test_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_fnmatch_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_rt_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_unix_signal_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/test
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ntest \
@@ -4711,7 +4709,7 @@ testlib_unix_hosts_linux_srcs = \
	$(STDLIB)/unix/hosts/+linux.ha \
	$(STDLIB)/unix/hosts/lookup.ha

$(TESTCACHE)/unix/hosts/unix_hosts-linux.ssa: $(testlib_unix_hosts_linux_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/unix/hosts/unix_hosts-linux.ssa: $(testlib_unix_hosts_linux_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/unix/hosts
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nunix::hosts \
@@ -4722,7 +4720,7 @@ testlib_unix_hosts_freebsd_srcs = \
	$(STDLIB)/unix/hosts/+freebsd.ha \
	$(STDLIB)/unix/hosts/lookup.ha

$(TESTCACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(testlib_unix_hosts_freebsd_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(testlib_unix_hosts_freebsd_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/unix/hosts
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nunix::hosts \
@@ -4734,7 +4732,7 @@ testlib_unix_passwd_any_srcs = \
	$(STDLIB)/unix/passwd/passwd.ha \
	$(STDLIB)/unix/passwd/types.ha

$(TESTCACHE)/unix/passwd/unix_passwd-any.ssa: $(testlib_unix_passwd_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/unix/passwd/unix_passwd-any.ssa: $(testlib_unix_passwd_any_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/unix/passwd
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nunix::passwd \
@@ -4767,7 +4765,7 @@ testlib_unix_resolvconf_linux_srcs = \
	$(STDLIB)/unix/resolvconf/+linux.ha \
	$(STDLIB)/unix/resolvconf/load.ha

$(TESTCACHE)/unix/resolvconf/unix_resolvconf-linux.ssa: $(testlib_unix_resolvconf_linux_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/unix/resolvconf/unix_resolvconf-linux.ssa: $(testlib_unix_resolvconf_linux_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/unix/resolvconf
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nunix::resolvconf \
@@ -4778,7 +4776,7 @@ testlib_unix_resolvconf_freebsd_srcs = \
	$(STDLIB)/unix/resolvconf/+freebsd.ha \
	$(STDLIB)/unix/resolvconf/load.ha

$(TESTCACHE)/unix/resolvconf/unix_resolvconf-freebsd.ssa: $(testlib_unix_resolvconf_freebsd_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
$(TESTCACHE)/unix/resolvconf/unix_resolvconf-freebsd.ssa: $(testlib_unix_resolvconf_freebsd_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/unix/resolvconf
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nunix::resolvconf \
@@ -4841,7 +4839,7 @@ $(TESTCACHE)/unix/tty/unix_tty-freebsd.ssa: $(testlib_unix_tty_freebsd_srcs) $(t
testlib_uuid_any_srcs = \
	$(STDLIB)/uuid/uuid.ha

$(TESTCACHE)/uuid/uuid-any.ssa: $(testlib_uuid_any_srcs) $(testlib_rt) $(testlib_crypto_random_$(PLATFORM)) $(testlib_strio_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strconv_$(PLATFORM))
$(TESTCACHE)/uuid/uuid-any.ssa: $(testlib_uuid_any_srcs) $(testlib_rt) $(testlib_crypto_random_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_endian_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_strconv_$(PLATFORM))
	@printf 'HAREC \t$@\n'
	@mkdir -p $(TESTCACHE)/uuid
	@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nuuid \
diff --git a/strings/template/template.ha b/strings/template/template.ha
index 2ce586bc..8fa1d1ec 100644
--- a/strings/template/template.ha
+++ b/strings/template/template.ha
@@ -4,8 +4,8 @@ use ascii;
use errors;
use fmt;
use io;
use memio;
use strings;
use strio;

export type literal = str;
export type variable = str;
@@ -19,7 +19,7 @@ export type param = (str, fmt::formattable);
// Compiles a template string. The return value must be freed with [[finish]]
// after use.
export fn compile(input: str) (template | errors::invalid) = {
	let buf = strio::dynamic();
	let buf = memio::dynamic();
	defer io::close(&buf)!;

	let instrs: []instruction = [];
@@ -36,12 +36,12 @@ export fn compile(input: str) (template | errors::invalid) = {
			match (strings::next(&iter)) {
			case let next_rn: rune =>
				if (next_rn == '$') {
					strio::appendrune(&buf, rn)!;
					memio::appendrune(&buf, rn)!;
				} else {
					strings::prev(&iter);
					const lit = strio::string(&buf);
					const lit = memio::string(&buf)!;
					append(instrs, strings::dup(lit): literal);
					strio::reset(&buf);
					memio::reset(&buf);

					parse_variable(&instrs, &iter, &buf)?;
				};
@@ -49,12 +49,12 @@ export fn compile(input: str) (template | errors::invalid) = {
				return errors::invalid;
			};
		} else {
			strio::appendrune(&buf, rn)!;
			memio::appendrune(&buf, rn)!;
		};
	};

	if (len(strio::string(&buf)) != 0) {
		const lit = strio::string(&buf);
	if (len(memio::string(&buf)!) != 0) {
		const lit = memio::string(&buf)!;
		append(instrs, strings::dup(lit): literal);
	};

@@ -109,7 +109,7 @@ fn get_param(name: str, params: param...) fmt::formattable = {
fn parse_variable(
	instrs: *[]instruction,
	iter: *strings::iterator,
	buf: *strio::stream,
	buf: *memio::stream,
) (void | errors::invalid) = {
	let brace = false;
	match (strings::next(iter)) {
@@ -135,13 +135,13 @@ fn parse_variable(
			if (rn == '{') {
				return errors::invalid;
			} else if (rn != '}') {
				strio::appendrune(buf, rn)!;
				memio::appendrune(buf, rn)!;
			} else {
				break;
			};
		} else {
			if (ascii::isalnum(rn)) {
				strio::appendrune(buf, rn)!;
				memio::appendrune(buf, rn)!;
			} else {
				strings::prev(iter);
				break;
@@ -149,9 +149,9 @@ fn parse_variable(
		};
	};

	const var = strio::string(buf);
	const var = memio::string(buf)!;
	append(instrs, strings::dup(var): variable);
	strio::reset(buf);
	memio::reset(buf);
};

def test_input: str = `Dear ${recipient},
@@ -170,7 +170,7 @@ foreign bank account balance of $1,000,000 to you.`;
	const tmpl = compile(test_input)!;
	defer finish(&tmpl);

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

	execute(&tmpl, &buf,
@@ -179,5 +179,5 @@ foreign bank account balance of $1,000,000 to you.`;
		("brother", "Elon Musk"),
	)!;

	assert(strio::string(&buf) == test_output);
	assert(memio::string(&buf)! == test_output);
};
diff --git a/strio/README b/strio/README
deleted file mode 100644
index 51befa20..00000000
--- a/strio/README
@@ -1,5 +0,0 @@
strio provides string-related I/O operations, specifically to make efficient use
of memory while building strings incrementally. The main entry points to strio
are [[dynamic]] and [[fixed]]. 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 strio or [[bufio]] stream.
diff --git a/strio/stream.ha b/strio/stream.ha
deleted file mode 100644
index 674b875b..00000000
--- a/strio/stream.ha
@@ -1,104 +0,0 @@
// License: MPL-2.0
// (c) 2022 Alexey Yerin <yyp@disroot.org>
// (c) 2021 Drew DeVault <sir@cmpwn.com>
use errors;
use io;
use slices;
use strings;

const fixed_vtable: io::vtable = io::vtable {
	writer = &fixed_write,
	...
};

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

// Returns the current contents of the buffer as a string. Aborts the program if
// invalid UTF-8 has been written to the buffer. The return value is borrowed
// from the stream, and will be freed when the stream is closed. Use
// [[strings::dup]] to extend its lifetime.
export fn string(in: *stream) str = {
	return strings::fromutf8(in.buf)!;
};

// Resets the buffer's length to zero, but does not attempt to deallocate its
// backing memory. Suitable for use both with fixed and dynamic streams.
export fn reset(in: *stream) void = {
	in.buf = in.buf[..0];
};

// Creates a write-only string stream using the provided buffer for storage.
// The writes will return an error if they would exceed the buffer's capacity.
// The stream doesn't need to be closed.
export fn fixed(in: []u8) stream = {
	return stream {
		stream = &fixed_vtable,
		buf = in[..0],
	};
};

fn fixed_write(s: *io::stream, buf: const []u8) (size | io::error) = {
	let s = s: *stream;
	let cap = slices::cap(s.buf);
	if (cap == len(s.buf)) return errors::overflow;
	let n = if (cap - len(s.buf) < len(buf)) cap - len(s.buf) else len(buf);
	static append(s.buf, buf[..n]...);
	return n;
};

@test fn fixed() void = {
	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(