about summary refs log tree commit diff
path: root/cmd
diff options
context:
space:
mode:
authorAlexey Yerin <yyp@disroot.org>2023-05-28 14:10:05 +0300
committerAlexey Yerin <yyp@disroot.org>2023-05-28 14:15:22 +0300
commit16e730f540acfe6a0015464c81831cdfacaf107b (patch)
treea2aa04bf788dc16977fc6cda2ed874938c3c13c5 /cmd
Initial commit
Diffstat (limited to 'cmd')
-rw-r--r--cmd/demo/main.ha58
-rw-r--r--cmd/demo4/main.ha59
-rw-r--r--cmd/hare-gi/context.ha129
-rw-r--r--cmd/hare-gi/ctype.ha263
-rw-r--r--cmd/hare-gi/emit.ha704
-rw-r--r--cmd/hare-gi/ident.ha144
-rw-r--r--cmd/hare-gi/main.ha148
-rw-r--r--cmd/hare-gi/populate.ha62
-rw-r--r--cmd/xmltree/main.ha104
9 files changed, 1671 insertions, 0 deletions
diff --git a/cmd/demo/main.ha b/cmd/demo/main.ha
new file mode 100644
index 0000000..cf807c6
--- /dev/null
+++ b/cmd/demo/main.ha
@@ -0,0 +1,58 @@
+use gio;
+use glib;
+use gobject;
+use gtk;
+use os;
+use rt;
+use types::c;
+
+fn about_clicked(btn: *gtk::Button, data: *void) void = {
+	let dialog = gtk::about_dialog_new();
+	const authors: []*c::char = [
+		c::fromstr("Harriet?"),
+		null: *c::char,
+	];
+	gtk::about_dialog_set_license_type(
+		dialog: *gtk::AboutDialog,
+		gtk::License::MPL_2_0,
+	);
+	gtk::about_dialog_set_authors(
+		dialog: *gtk::AboutDialog,
+		authors: *[*]*c::char: **c::char,
+	);
+	gtk::about_dialog_set_website(
+		dialog: *gtk::AboutDialog,
+		c::fromstr("https://git.sr.ht/~yerinalexey/hare-gi"),
+	);
+
+	gtk::widget_show_all(dialog);
+};
+
+fn activate(app: *gio::Application, data: *void) void = {
+	const app = app: *gtk::Application;
+
+	let button = gtk::button_new_with_label(c::fromstr("About"));
+	gtk::button_connect_clicked(button: *gtk::Button, &about_clicked, null);
+
+	let headerbar = gtk::header_bar_new();
+	gtk::header_bar_set_title(headerbar: *gtk::HeaderBar, c::fromstr("hare-gi demo"));
+	gtk::header_bar_set_show_close_button(headerbar: *gtk::HeaderBar, glib::TRUE);
+	gtk::header_bar_pack_end(headerbar: *gtk::HeaderBar, button);
+
+	let window = gtk::application_window_new(app);
+	gtk::window_set_titlebar(window: *gtk::Window, headerbar);
+	gtk::widget_show_all(window);
+};
+
+export fn main() void = {
+	let app = gtk::application_new(
+		c::fromstr("hare_gi.example"),
+		gio::ApplicationFlags::FLAGS_NONE,
+	): *gio::Application;
+	gio::application_connect_activate(app, &activate, null);
+	const status = gio::application_run(
+		app, rt::argc: int, rt::argv: **c::char,
+	);
+	gobject::object_unref(app);
+	os::exit(status);
+};
diff --git a/cmd/demo4/main.ha b/cmd/demo4/main.ha
new file mode 100644
index 0000000..c46bed2
--- /dev/null
+++ b/cmd/demo4/main.ha
@@ -0,0 +1,59 @@
+use gio;
+use glib;
+use gobject;
+use gtk4;
+use os;
+use rt;
+use types::c;
+
+fn about_clicked(btn: *gtk4::Button, data: *void) void = {
+	let dialog = gtk4::about_dialog_new();
+	const authors: []*c::char = [
+		c::fromstr("Harriet?"),
+		null: *c::char,
+	];
+	gtk4::about_dialog_set_license_type(
+		dialog: *gtk4::AboutDialog,
+		gtk4::License::MPL_2_0,
+	);
+	gtk4::about_dialog_set_authors(
+		dialog: *gtk4::AboutDialog,
+		authors: *[*]*c::char: **c::char,
+	);
+	gtk4::about_dialog_set_website(
+		dialog: *gtk4::AboutDialog,
+		c::fromstr("https://git.sr.ht/~yerinalexey/hare-gi"),
+	);
+
+	gtk4::widget_show(dialog);
+};
+
+fn activate(app: *gio::Application, data: *void) void = {
+	const app = app: *gtk4::Application;
+
+	let button = gtk4::button_new_with_label(c::fromstr("About"));
+	gtk4::button_connect_clicked(button: *gtk4::Button, &about_clicked, null);
+
+	let headerbar = gtk4::header_bar_new();
+	let title = gtk4::label_new(c::fromstr("hare-gi demo"));
+	gtk4::header_bar_set_title_widget(headerbar: *gtk4::HeaderBar, title);
+	gtk4::header_bar_set_show_title_buttons(headerbar: *gtk4::HeaderBar, glib::TRUE);
+	gtk4::header_bar_pack_end(headerbar: *gtk4::HeaderBar, button);
+
+	let window = gtk4::application_window_new(app);
+	gtk4::window_set_titlebar(window: *gtk4::Window, headerbar);
+	gtk4::widget_show(window);
+};
+
+export fn main() void = {
+	let app = gtk4::application_new(
+		c::fromstr("hare_gi.example"),
+		gio::ApplicationFlags::FLAGS_NONE,
+	): *gio::Application;
+	gio::application_connect_activate(app, &activate, null);
+	const status = gio::application_run(
+		app, rt::argc: int, rt::argv: **c::char,
+	);
+	gobject::object_unref(app);
+	os::exit(status);
+};
diff --git a/cmd/hare-gi/context.ha b/cmd/hare-gi/context.ha
new file mode 100644
index 0000000..d4f47fa
--- /dev/null
+++ b/cmd/hare-gi/context.ha
@@ -0,0 +1,129 @@
+use fmt;
+use gir;
+use io;
+use os;
+use strings;
+
+type namespace = struct {
+	gir::namespace,
+	includes: []gir::include,
+
+	// C -> Hare type mappings
+	types: [](str, str),
+	// All imported modules. Populated during emit_empty()
+	imports: []ident,
+	// A list of exported symbols to make sure nothing is emitted twice
+	exports: []str,
+
+	module: ident,
+	output_file: io::file,
+	output: io::handle,
+};
+fn namespace_finish(ns: *namespace) void = {
+	gir::namespace_finish(ns);
+	if (ns.output_file != -1) {
+		io::close(ns.output_file)!;
+	};
+	for (let i = 0z; i < len(ns.includes); i += 1) {
+		gir::include_finish(&ns.includes[i]);
+	};
+	free(ns.includes);
+	for (let i = 0z; i < len(ns.types); i += 1) {
+		// types[i].0 is borrowed from GIR structures
+		free(ns.types[i].1);
+	};
+	free(ns.types);
+	for (let i = 0z; i < len(ns.imports); i += 1) {
+		strings::freeall(ns.imports[i]);
+	};
+	free(ns.exports);
+	free(ns.module);
+};
+
+type context = struct {
+	namespaces: []namespace,
+	current: *namespace,
+	stack: []str,
+	brief: bool,
+
+	// Core modules
+	glib: *namespace,
+	gobject: *namespace,
+};
+fn context_finish(ctx: *context) void = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		namespace_finish(&ctx.namespaces[i]);
+	};
+	free(ctx.namespaces);
+};
+
+fn add_repository(ctx: *context, repo: gir::repository) void = {
+	for (let i = 0z; i < len(repo.namespaces); i += 1) {
+		let new = namespace {
+			output_file = -1: io::file,
+			output = io::empty,
+			...
+		};
+		*(&new: *gir::namespace) = repo.namespaces[i];
+		append(new.includes, repo.includes...);
+		append(ctx.namespaces, new);
+	};
+};
+
+fn get_namespace(ctx: *context, name: str, version: str...) nullable *namespace = {
+	assert(len(version) <= 1);
+	if (len(version) == 1) {
+		for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+			if (ctx.namespaces[i].name == name
+					&& ctx.namespaces[i].version == version[0]) {
+				return &ctx.namespaces[i];
+			};
+		};
+	} else {
+		for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+			if (ctx.namespaces[i].name == name) {
+				return &ctx.namespaces[i];
+			};
+		};
+	};
+	return null;
+};
+
+fn add_import(ns: *namespace, module: ident) void = {
+	for (let i = 0z; i < len(ns.imports); i += 1) {
+		if (ident_eq(ns.imports[i], module)) {
+			return;
+		};
+	};
+	append(ns.imports, strings::dupall(module));
+};
+
+fn lookup_type(ctx: *context, name: str) ((*namespace, str) | void) = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+		match (lookup_type_in_namespace(ns, name)) {
+		case let ident: str =>
+			return (ns, ident);
+		case void =>
+			yield;
+		};
+	};
+};
+
+fn lookup_type_in_namespace(ns: *namespace, name: str) (str | void) = {
+	for (let i = 0z; i < len(ns.types); i += 1) {
+		// NOTE: sometimes the requested type doesn't have a prefix
+		// (like Gtk from GtkWindow), the second check catches that
+		if (ns.types[i].0 == name || ns.types[i].1 == name) {
+			return ns.types[i].1;
+		};
+	};
+};
+
+fn push(ctx: *context, name: str) void = {
+	append(ctx.stack, name);
+};
+fn pop(ctx: *context) void = {
+	assert(len(ctx.stack) > 0);
+	delete(ctx.stack[len(ctx.stack) - 1]);
+};
diff --git a/cmd/hare-gi/ctype.ha b/cmd/hare-gi/ctype.ha
new file mode 100644
index 0000000..f45dc9b
--- /dev/null
+++ b/cmd/hare-gi/ctype.ha
@@ -0,0 +1,263 @@
+// The world's worst C parser
+
+use ascii;
+use strings;
+use strio;
+use fmt;
+
+type ctype = ((cmodule, str) | str | cbuiltin | cpointer);
+type cpointer = *ctype;
+type cbuiltin = enum {
+	VOID,
+	INT,
+	UINT,
+	BOOL,
+	SIZE,
+	RUNE,
+	VALIST,
+	VOID_POINTER,
+
+	CHAR,
+	UCHAR,
+	SHORT,
+	USHORT,
+	LONG,
+	ULONG,
+	SSIZE,
+
+	I8, U8,
+	I16, U16,
+	I32, U32,
+	I64, U64,
+	F32, F64,
+};
+type cmodule = enum {
+	LIBC,
+	GLIB,
+	GOBJECT,
+};
+
+fn ctype_finish(type_: ctype) void = {
+	match (type_) {
+	case let pointer: cpointer =>
+		ctype_finish(*pointer);
+		free(pointer);
+	case let s: str =>
+		free(s);
+	case => yield;
+	};
+};
+
+fn cbuiltin_str(b: cbuiltin) const str = switch (b) {
+case cbuiltin::VOID => yield "void";
+case cbuiltin::INT => yield "int";
+case cbuiltin::UINT => yield "uint";
+case cbuiltin::BOOL => yield "bool";
+case cbuiltin::SIZE => yield "size";
+case cbuiltin::RUNE => yield "rune";
+case cbuiltin::VALIST => yield "valist";
+case cbuiltin::VOID_POINTER => yield "*void";
+
+case cbuiltin::CHAR => yield "c::char";
+case cbuiltin::UCHAR => yield "c::uchar";
+case cbuiltin::SSIZE => yield "c::ssize";
+case cbuiltin::SHORT => yield "c::short";
+case cbuiltin::USHORT => yield "c::ushort";
+case cbuiltin::LONG => yield "c::long";
+case cbuiltin::ULONG => yield "c::ulong";
+
+case cbuiltin::I8 => yield "i8";
+case cbuiltin::U8 => yield "u8";
+case cbuiltin::I16 => yield "i16";
+case cbuiltin::U16 => yield "u16";
+case cbuiltin::I32 => yield "i32";
+case cbuiltin::U32 => yield "u32";
+case cbuiltin::I64 => yield "i64";
+case cbuiltin::U64 => yield "u64";
+case cbuiltin::F32 => yield "f32";
+case cbuiltin::F64 => yield "f64";
+};
+
+fn cbuiltin_unsigned(b: cbuiltin) cbuiltin = switch (b) {
+case cbuiltin::INT => yield cbuiltin::UINT;
+case cbuiltin::CHAR => yield cbuiltin::UCHAR;
+case cbuiltin::SHORT => yield cbuiltin::USHORT;
+case cbuiltin::LONG => yield cbuiltin::ULONG;
+case => abort();
+};
+
+fn cbuiltin_needs_import(b: cbuiltin) bool = switch (b) {
+case cbuiltin::CHAR, cbuiltin::UCHAR, cbuiltin::SHORT, cbuiltin::USHORT,
+cbuiltin::LONG, cbuiltin::ULONG, cbuiltin::SSIZE => yield true;
+case => yield false;
+};
+
+let map: [](str, ctype) = [
+	("void", cbuiltin::VOID),
+	("int", cbuiltin::INT),
+	("char", cbuiltin::CHAR),
+	("short", cbuiltin::SHORT),
+	("long", cbuiltin::LONG),
+	("float", cbuiltin::F32),
+	("double", cbuiltin::F64),
+
+	// C11, <stdbool.h>
+	("_Bool", cbuiltin::BOOL),
+	("bool", cbuiltin::BOOL),
+
+	// <stdarg.h>
+	("va_list", cbuiltin::VALIST),
+
+	// <stdlib.h>
+	("size_t", cbuiltin::SIZE),
+	("ssize_t", cbuiltin::SSIZE),
+
+	// <stdint.h>
+	("int8_t", cbuiltin::I8), ("uint8_t", cbuiltin::U8),
+	("int16_t", cbuiltin::I16), ("uint16_t", cbuiltin::U16),
+	("int32_t", cbuiltin::I32), ("uint32_t", cbuiltin::U32),
+	("int64_t", cbuiltin::I64), ("uint64_t", cbuiltin::U64),
+
+	// <glib/gtypes.h>
+	("gchar", cbuiltin::CHAR),
+	("gshort", cbuiltin::SHORT),
+	("glong", cbuiltin::LONG),
+	("gint", cbuiltin::INT),
+	("gboolean", (cmodule::GLIB, "boolean")),
+	("guchar", cbuiltin::UCHAR),
+	("gushort", cbuiltin::USHORT),
+	("gulong", cbuiltin::ULONG),
+	("guint", cbuiltin::UINT),
+	("gfloat", cbuiltin::F32),
+	("gdouble", cbuiltin::F64),
+	("gpointer", cbuiltin::VOID_POINTER),
+	("gconstpointer", cbuiltin::VOID_POINTER),
+	("grefcount", cbuiltin::UINT),
+	("gatomicrefcount", cbuiltin::UINT),
+
+	("gint8", cbuiltin::I8), ("guint8", cbuiltin::U8),
+	("gint16", cbuiltin::I16), ("guint16", cbuiltin::U16),
+	("gint32", cbuiltin::I32), ("guint32", cbuiltin::U32),
+	("gint64", cbuiltin::I64), ("guint64", cbuiltin::U64),
+	("gsize", cbuiltin::SIZE), ("gssize", cbuiltin::SSIZE),
+	("goffset", cbuiltin::I64),
+
+	// <glib/gunicode.h>
+	("gunichar", cbuiltin::RUNE),
+	("gunichar2", cbuiltin::U16),
+
+	// <gobject/gvalue.h>
+	("_Value__data__union", (cmodule::GOBJECT, "ValueUnion")),
+
+	// <unistd.h>
+	("pid_t", cbuiltin::UINT),
+	("uid_t", cbuiltin::UINT),
+
+	// <time.h>
+	("time_t", (cmodule::LIBC, "time_t")),
+	("tm", (cmodule::LIBC, "tm")),
+
+	// <stdio.h>
+	("FILE", (cmodule::LIBC, "FILE")),
+
+	// <pwd.h>
+	("passwd", (cmodule::LIBC, "passwd")),
+
+	// freetype
+	("int32", cbuiltin::I32),
+
+	// ("utf8", ...) - see resolve()
+];
+
+fn parse_ctype(type_: str) ctype = {
+	let iter = strings::iter(type_);
+	let current: ctype = "";
+
+	let unsigned = false;
+	for (true) match (next_token(&iter)) {
+	case let s: str =>
+		if (s == "const" || s == "volatile") {
+			continue;
+		};
+		if (s == "unsigned") {
+			unsigned = true;
+		} else {
+			current = resolve(s);
+			if (unsigned) {
+				current = cbuiltin_unsigned(current as cbuiltin);
+				unsigned = false;
+			};
+		};
+	case let r: rune =>
+		switch (r) {
+		case '*' =>
+			current = alloc(current): cpointer: ctype;
+		case => abort();
+		};
+	case =>
+		break;
+	};
+	if (unsigned) {
+		return cbuiltin::UINT;
+	};
+	return current;
+};
+
+fn resolve(s: str) ctype = {
+	if (s == "utf8") {
+		return alloc(cbuiltin::CHAR: ctype): cpointer;
+	};
+	for (let i = 0z; i < len(map); i += 1) {
+		if (map[i].0 == s) {
+			return map[i].1;
+		};
+	};
+	return s;
+};
+
+fn next_token(iter: *strings::iterator) (str | rune | void) = {
+	for (true) match (_next_token(iter)) {
+	case let s: str =>
+		return s;
+	case let r: rune =>
+		return r;
+	case =>
+		break;
+	};
+};
+
+fn _next_token(iter: *strings::iterator) (str | rune | void) = {
+	let buffer = strio::dynamic();
+	let first = true;
+	for (true) match (strings::next(iter)) {
+	case let r: rune =>
+		if (ascii::isspace(r)) {
+			if (len(buffer.buf) > 0) {
+				return strio::string(&buffer);
+			};
+			continue;
+		};
+		if (is_ident(r, first)) {
+			strio::appendrune(&buffer, r)!;
+			first = false;
+		} else switch (r) {
+		case '*' =>
+			if (len(buffer.buf) > 0) {
+				strings::prev(iter);
+				return strio::string(&buffer);
+			} else {
+				return r;
+			};
+		case =>
+			fmt::fatalf("Unexpected character in a type: '{}'", r);
+		};
+	case =>
+		break;
+	};
+	if (len(buffer.buf) > 0) {
+		return strio::string(&buffer);
+	};
+};
+
+fn is_ident(r: rune, first: bool) bool
+	= ascii::isalpha(r) || r == '_' || (!first && ascii::isdigit(r));
diff --git a/cmd/hare-gi/emit.ha b/cmd/hare-gi/emit.ha
new file mode 100644
index 0000000..fd37110
--- /dev/null
+++ b/cmd/hare-gi/emit.ha
@@ -0,0 +1,704 @@
+use ascii;
+use bufio;
+use fmt;
+use gir;
+use io;
+use os;
+use strings;
+
+// Second stage of codegen: generate code for every namespace, but discard it.
+// This will fill namespace.imports list, which will be used in the next stage.
+fn emit_empty(ctx: *context) void = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+		ctx.current = ns;
+		ctx.current.output = io::empty;
+		emit_namespace(ctx, ns)!;
+	};
+};
+
+// Third stage: generate the final code for modules that have specified an
+// output file.
+fn emit(ctx: *context) (void | io::error) = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+
+		ctx.current = ns;
+
+		if (ns.output_file == -1) continue;
+
+		static let wbuf: [os::BUFSIZ]u8 = [0...];
+		const stream = bufio::buffered(ns.output_file, [], wbuf);
+		ctx.current.output = &stream;
+
+		delete(ctx.current.exports[..]);
+		emit_namespace(ctx, ns)?;
+	};
+};
+
+fn emit_namespace(ctx: *context, ns: *namespace) (void | io::error) = {
+	defer assert(len(ctx.stack) == 0);
+
+	for (let i = 0z; i < len(ns.imports); i += 1) {
+		fmt::fprint(ctx.current.output, "use ")?;
+		emit_ident(ctx.current.output, ns.imports[i])?;
+		fmt::fprintln(ctx.current.output, ";")?;
+	};
+
+	for (let i = 0z; i < len(ns.aliases); i += 1) {
+		emit_alias(ctx, &ns.aliases[i])?;
+	};
+	for (let i = 0z; i < len(ns.classes); i += 1) {
+		emit_class(ctx, &ns.classes[i])?;
+	};
+	for (let i = 0z; i < len(ns.interfaces); i += 1) {
+		emit_interface(ctx, &ns.interfaces[i])?;
+	};
+	for (let i = 0z; i < len(ns.records); i += 1) {
+		emit_record(ctx, &ns.records[i])?;
+	};
+	for (let i = 0z; i < len(ns.enums); i += 1) {
+		emit_enumeration(ctx, &ns.enums[i])?;
+	};
+	for (let i = 0z; i < len(ns.functions); i += 1) {
+		emit_function(ctx, &ns.functions[i])?;
+	};
+	for (let i = 0z; i < len(ns.unions); i += 1) {
+		emit_union(ctx, &ns.unions[i])?;
+	};
+	for (let i = 0z; i < len(ns.bitfields); i += 1) {
+		emit_bitfield(ctx, &ns.bitfields[i])?;
+	};
+	for (let i = 0z; i < len(ns.callbacks); i += 1) {
+		emit_callback(ctx, &ns.callbacks[i])?;
+	};
+	for (let i = 0z; i < len(ns.constants); i += 1) {
+		emit_constant(ctx, &ns.constants[i])?;
+	};
+};
+
+fn emit_alias(ctx: *context, alias: *gir::alias) (void | io::error) = {
+	if (is_exported(ctx, alias.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, alias.c_type);
+
+	emit_doc(ctx, alias)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(alias.name))?;
+	emit_type(ctx, alias.inner)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_class(ctx: *context, class: *gir::class) (void | io::error) = {
+	if (is_exported(ctx, class.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, class.c_type);
+
+	emit_doc(ctx, class)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(class.name))?;
+	if (len(class.entries) == 0) {
+		// opaque
+		fmt::fprintln(ctx.current.output, "*void;")?;
+	} else {
+		fmt::fprintln(ctx.current.output, "struct {")?;
+		for (let i = 0z; i < len(class.entries); i += 1) {
+			emit_entry(ctx, &class.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	// TODO: add implements into documentation
+
+	push(ctx, class.name);
+	for (let i = 0z; i < len(class.constructors); i += 1) {
+		emit_constructor(ctx, &class.constructors[i])?;
+	};
+	for (let i = 0z; i < len(class.methods); i += 1) {
+		emit_method(ctx, &class.methods[i])?;
+	};
+	for (let i = 0z; i < len(class.functions); i += 1) {
+		emit_function(ctx, &class.functions[i])?;
+	};
+	// Ignore: class.virtual_methods
+	// Ignore: class.properties
+	for (let i = 0z; i < len(class.signals); i += 1) {
+		emit_signal(ctx, &class.signals[i])?;
+	};
+	for (let i = 0z; i < len(class.constants); i += 1) {
+		emit_constant(ctx, &class.constants[i])?;
+	};
+	// Unused: class.callbacks
+	assert(len(class.callbacks) == 0);
+	pop(ctx);
+};
+
+fn emit_interface(ctx: *context, iface: *gir::interface) (void | io::error) = {
+	if (is_exported(ctx, iface.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, iface.c_type);
+
+	emit_doc(ctx, iface)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(iface.name))?;
+	if (len(iface.entries) == 0) {
+		// opaque
+		fmt::fprintln(ctx.current.output, "*void;")?;
+	} else {
+		fmt::fprintln(ctx.current.output, "struct {")?;
+		for (let i = 0z; i < len(iface.entries); i += 1) {
+			emit_entry(ctx, &iface.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	// TODO: add implements and prerequisites into documentation
+
+	push(ctx, iface.name);
+	for (let i = 0z; i < len(iface.constructors); i += 1) {
+		emit_constructor(ctx, &iface.constructors[i])?;
+	};
+	for (let i = 0z; i < len(iface.methods); i += 1) {
+		emit_method(ctx, &iface.methods[i])?;
+	};
+	for (let i = 0z; i < len(iface.functions); i += 1) {
+		emit_function(ctx, &iface.functions[i])?;
+	};
+	// Ignore: iface.virtual_methods
+	// Ignore: iface.properties
+	for (let i = 0z; i < len(iface.signals); i += 1) {
+		emit_signal(ctx, &iface.signals[i])?;
+	};
+	for (let i = 0z; i < len(iface.constants); i += 1) {
+		emit_constant(ctx, &iface.constants[i])?;
+	};
+	// Unused: iface.callbacks
+	assert(len(iface.callbacks) == 0);
+	pop(ctx);
+};
+
+fn emit_union(ctx: *context, union_: *gir::union_) (void | io::error) = {
+	if (is_exported(ctx, union_.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, union_.c_type);
+
+	emit_doc(ctx, union_)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(union_.name))?;
+	if (len(union_.entries) == 0) {
+		fmt::fprintln(ctx.current.output, "void;")?; // FIXME
+	} else {
+		fmt::fprintln(ctx.current.output, "union {")?;
+		for (let i = 0z; i < len(union_.entries); i += 1) {
+			emit_entry(ctx, &union_.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	push(ctx, union_.name);
+	for (let i = 0z; i < len(union_.constructors); i += 1) {
+		emit_constructor(ctx, &union_.constructors[i])?;
+	};
+	for (let i = 0z; i < len(union_.methods); i += 1) {
+		emit_method(ctx, &union_.methods[i])?;
+	};
+	for (let i = 0z; i < len(union_.functions); i += 1) {
+		emit_function(ctx, &union_.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_record(ctx: *context, record: *gir::record) (void | io::error) = {
+	if (is_exported(ctx, record.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, record.c_type);
+
+	emit_doc(ctx, record)?;
+	fmt::fprintf(ctx.current.output, "export type {} = ",
+		fix_identifier(record.name))?;
+	if (record.opaque || len(record.entries) == 0) {
+		fmt::fprintln(ctx.current.output, "*void;")?;
+	} else {
+		fmt::fprintln(ctx.current.output, "struct {")?;
+		for (let i = 0z; i < len(record.entries); i += 1) {
+			emit_entry(ctx, &record.entries[i])?;
+		};
+		fmt::fprintln(ctx.current.output, "};")?;
+	};
+
+	push(ctx, record.name);
+	for (let i = 0z; i < len(record.constructors); i += 1) {
+		emit_constructor(ctx, &record.constructors[i])?;
+	};
+	for (let i = 0z; i < len(record.methods); i += 1) {
+		emit_method(ctx, &record.methods[i])?;
+	};
+	for (let i = 0z; i < len(record.functions); i += 1) {
+		emit_function(ctx, &record.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_enumeration(
+	ctx: *context,
+	enumeration: *gir::enumeration,
+) (void | io::error) = {
+	if (is_exported(ctx, enumeration.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, enumeration.c_type);
+
+	emit_doc(ctx, enumeration)?;
+	fmt::fprintfln(ctx.current.output, "export type {} = enum uint {{",
+		fix_identifier(enumeration.name))?;
+	for (let i = 0z; i < len(enumeration.members); i += 1) {
+		emit_member(ctx, &enumeration.members[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "};")?;
+
+	push(ctx, enumeration.name);
+	for (let i = 0z; i < len(enumeration.functions); i += 1) {
+		emit_function(ctx, &enumeration.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_bitfield(ctx: *context, bitfield: *gir::bitfield) (void | io::error) = {
+	if (is_exported(ctx, bitfield.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, bitfield.c_type);
+
+	emit_doc(ctx, bitfield)?;
+	fmt::fprintfln(ctx.current.output, "export type {} = enum uint {{",
+		fix_identifier(bitfield.name))?;
+	for (let i = 0z; i < len(bitfield.members); i += 1) {
+		emit_member(ctx, &bitfield.members[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "};")?;
+
+	push(ctx, bitfield.name);
+	for (let i = 0z; i < len(bitfield.functions); i += 1) {
+		emit_function(ctx, &bitfield.functions[i])?;
+	};
+	pop(ctx);
+};
+
+fn emit_member(ctx: *context, member: *gir::member) (void | io::error) = {
+	emit_indented_doc(ctx, member)?;
+	fmt::fprintfln(ctx.current.output, "\t" "{} = {},",
+		to_uppercase(fix_identifier(member.name)), member.value)?;
+};
+
+fn emit_callback(ctx: *context, callback: *gir::callback) (void | io::error) = {
+	if (is_exported(ctx, callback.c_type)) {
+		return;
+	};
+	append(ctx.current.exports, callback.c_type);
+
+	emit_doc(ctx, callback)?;
+	fmt::fprintf(ctx.current.output, "export type {} = *fn(",
+		fix_identifier(callback.name))?;
+	emit_params(ctx, callback.params)?;
+	if (callback.throws) {
+		if (len(callback.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, callback.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_constant(ctx: *context, constant: *gir::constant) (void | io::error) = {
+	// TODO: constants
+	return;
+};
+
+fn emit_constructor(
+	ctx: *context,
+	constructor: *gir::constructor,
+) (void | io::error) = {
+	if (is_exported(ctx, constructor.c_identifier)) {
+		return;
+	};
+	append(ctx.current.exports, constructor.c_identifier);
+
+	emit_doc(ctx, constructor)?;
+	fmt::fprintf(ctx.current.output, `export @symbol("{}") fn `,
+		constructor.c_identifier)?;
+	emit_function_name(ctx, constructor.name)?;
+	fmt::fprint(ctx.current.output, "(")?;
+	emit_params(ctx, constructor.params)?;
+	if (constructor.throws) {
+		if (len(constructor.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, constructor.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_method(ctx: *context, method: *gir::method) (void | io::error) = {
+	if (is_exported(ctx, method.c_identifier)) {
+		return;
+	};
+	append(ctx.current.exports, method.c_identifier);
+
+	emit_doc(ctx, method)?;
+	fmt::fprintf(ctx.current.output, `export @symbol("{}") fn `,
+		method.c_identifier)?;
+	emit_function_name(ctx, method.name)?;
+	fmt::fprint(ctx.current.output, "(")?;
+	emit_instance_param(ctx, method.instance)?;
+	if (len(method.params) > 0) {
+		fmt::fprint(ctx.current.output, ", ")?;
+	};
+	emit_params(ctx, method.params)?;
+	if (method.throws) {
+		fmt::fprint(ctx.current.output, ", error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, method.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_function(ctx: *context, function: *gir::function) (void | io::error) = {
+	if (is_exported(ctx, function.c_identifier)) {
+		return;
+	};
+	append(ctx.current.exports, function.c_identifier);
+
+	emit_doc(ctx, function)?;
+	fmt::fprintf(ctx.current.output, `export @symbol("{}") fn `,
+		function.c_identifier)?;
+	emit_function_name(ctx, function.name)?;
+	fmt::fprint(ctx.current.output, "(")?;
+	emit_params(ctx, function.params)?;
+	if (function.throws) {
+		if (len(function.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, function.return_value)?;
+	fmt::fprintln(ctx.current.output, ";")?;
+};
+
+fn emit_signal(ctx: *context, signal: *gir::signal) (void | io::error) = {
+	assert(len(ctx.stack) >= 1);
+	// Example code for GtkWindow::activate:
+	//
+	// export fn window_connect_activate(
+	// 	instance: *gtk::Window,
+	// 	handler: *fn(instance: *gtk::Window, data: *void) void,
+	// 	data: nullable *void,
+	// ) u64 = gobject::signal_connect_data(
+	// 	instance,
+	// 	*(&"activate\0": []u8): *[*]u8: *c::char,
+	// 	handler: gobject::Callback,
+	// 	data: *void,
+	// 	null: gobject::ClosureNotify, 0,
+	// );
+
+	// TODO: add documentation
+
+	const function = strings::concat(
+		"connect_",
+		normalize_signal(signal.name),
+	);
+	defer free(function);
+
+	fmt::fprintf(ctx.current.output, "export fn ")?;
+	emit_function_name(ctx, function)?;
+	fmt::fprintln(ctx.current.output, "(")?;
+	fmt::fprintfln(ctx.current.output, "\t" "instance: *{},", ctx.stack[0])?;
+	fmt::fprintf(ctx.current.output,
+		"\t" "handler: *fn(instance: *{}",
+		ctx.stack[0],
+	)?;
+	if (len(signal.params) > 0) {
+		fmt::fprint(ctx.current.output, ", ")?;
+		emit_params(ctx, signal.params)?;
+	};
+	fmt::fprint(ctx.current.output, ", data: *void) ")?;
+	emit_return_value(ctx, signal.return_value)?;
+	fmt::fprintln(ctx.current.output, ",")?;
+	fmt::fprintln(ctx.current.output, "\t" "data: nullable *void,")?;
+	fmt::fprint(ctx.current.output, ") u64 = ")?;
+	emit_object(ctx, ctx.gobject, "signal_connect_data")?;
+	fmt::fprintln(ctx.current.output, "(")?;
+	fmt::fprintln(ctx.current.output,  "\t" "instance,")?;
+	fmt::fprintfln(ctx.current.output,
+		"\t" `*(&"{}\0": *[]u8): *[*]u8: *c::char,`,
+		signal.name,
+	)?;
+	fmt::fprint(ctx.current.output,    "\t" "handler: ")?;
+	emit_object(ctx, ctx.gobject, "Callback")?;
+	fmt::fprintln(ctx.current.output, ",")?;
+	fmt::fprintln(ctx.current.output,  "\t" "data: *void,")?;
+	fmt::fprint(ctx.current.output,    "\t" "null: ")?;
+	emit_object(ctx, ctx.gobject, "ClosureNotify")?;
+	fmt::fprintln(ctx.current.output, ", 0,")?;
+	fmt::fprintln(ctx.current.output, ");")?;
+};
+
+fn emit_instance_param(
+	ctx: *context,
+	instance: gir::instance_parameter,
+) (void | io::error) = {
+	fmt::fprintf(ctx.current.output, "{}: ", fix_identifier(instance.name))?;
+	emit_type(ctx, instance.type_)?;
+};
+
+fn emit_params(ctx: *context, params: []gir::callable_param) (void | io::error) = {
+	for (let i = 0z; i < len(params); i += 1) {
+		const param = params[i];
+		if (i > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		match (param.parameter) {
+		case let t: gir::any_type =>
+			fmt::fprintf(ctx.current.output, "{}: ",
+				fix_identifier(param.name))?;
+			emit_type(ctx, t)?;
+		case gir::varargs =>
+			fmt::fprint(ctx.current.output, "...")?;
+			assert(i == len(params) - 1);
+		};
+	};
+};
+
+fn emit_return_value(
+	ctx: *context,
+	ret: (gir::callable_return | void),
+) (void | io::error) = {
+	match (ret) {
+	case let ret: gir::callable_return =>
+		emit_type(ctx, ret.type_)?;
+	case =>
+		fmt::fprint(ctx.current.output, "void")?;
+	};
+};
+
+fn emit_function_name(ctx: *context, name: str) (void | io::error) = {
+	if (len(ctx.stack) == 0) {
+		fmt::fprint(ctx.current.output, fix_identifier(name))?;
+		return;
+	};
+
+	const path = strings::concat(ctx.stack...);
+	defer free(path);
+	const lower = swap_case(path);
+	defer free(lower);
+
+	fmt::fprintf(ctx.current.output, "{}_{}", lower, name)?;
+};
+
+fn emit_entry(ctx: *context, entry: *gir::entry) (void | io::error) = {
+	match (*entry) {
+	case let f: gir::field =>
+		emit_field(ctx, &f)?;
+	case let u: gir::union_ =>
+		emit_inline_union(ctx, &u)?;
+	case let r: gir::record =>
+		emit_inline_record(ctx, &r)?;
+	};
+};
+
+fn emit_field(ctx: *context, field: *gir::field) (void | io::error) = {
+	emit_indented_doc(ctx, field)?;
+	fmt::fprintf(ctx.current.output, "\t" "{}: ",
+		fix_identifier(field.name))?;
+	if (!field.writable) {
+		fmt::fprint(ctx.current.output, "const ")?;
+	};
+	emit_type(ctx, field.type_)?;
+	fmt::fprintln(ctx.current.output, ",")?;
+};
+
+fn emit_inline_union(ctx: *context, union_: *gir::union_) (void | io::error) = {
+	emit_indented_doc(ctx, union_)?;
+	fmt::fprint(ctx.current.output, "\t")?;
+	if (len(union_.name) > 0) {
+		fmt::fprintf(ctx.current.output, "{}: ",
+			fix_identifier(union_.name))?;
+	};
+	fmt::fprintln(ctx.current.output, "union {")?;
+	for (let i = 0z; i < len(union_.entries); i += 1) {
+		emit_entry(ctx, &union_.entries[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "},")?;
+};
+
+fn emit_inline_record(ctx: *context, record: *gir::record) (void | io::error) = {
+	emit_indented_doc(ctx, record)?;
+	fmt::fprint(ctx.current.output, "\t")?;
+	if (len(record.name) > 0) {
+		fmt::fprintf(ctx.current.output, "{}: ",
+			fix_identifier(record.name))?;
+	};
+	fmt::fprintln(ctx.current.output, "struct {")?;
+	for (let i = 0z; i < len(record.entries); i += 1) {
+		emit_entry(ctx, &record.entries[i])?;
+	};
+	fmt::fprintln(ctx.current.output, "},")?;
+};
+
+fn emit_indented_doc(ctx: *context, item: *gir::documentation) (void | io::error) = {
+	if (ctx.brief) {
+		return;
+	};
+	const iter = strings::tokenize(item.doc, "\n");
+	for (true) match (strings::next_token(&iter)) {
+	case let line: str =>
+		fmt::fprintln(ctx.current.output, "\t" "//", line)?;
+	case =>
+		break;
+	};
+};
+
+fn emit_doc(ctx: *context, item: *gir::documentation) (void | io::error) = {
+	if (ctx.brief) {
+		return;
+	};
+	const iter = strings::tokenize(item.doc, "\n");
+	for (true) match (strings::next_token(&iter)) {
+	case let line: str =>
+		fmt::fprintln(ctx.current.output, "//", line)?;
+	case =>
+		break;
+	};
+};
+
+fn emit_type(ctx: *context, t: (gir::any_type | gir::callback)) (void | io::error) = {
+	match (t) {
+	case let t: gir::simple_type =>
+		if (len(t.c_type) > 0) {
+			const parsed = parse_ctype(t.c_type);
+			defer ctype_finish(parsed);
+			return emit_c_type(ctx, parsed);
+		} else {
+			const (first, second) = strings::cut(t.name, ".");
+			if (len(second) == 0) {
+				const parsed = parse_ctype(t.name);
+				defer ctype_finish(parsed);
+				return emit_c_type(ctx, parsed);
+			} else {
+				const ns = match (get_namespace(ctx, first)) {
+				case let ns: *namespace =>
+					yield ns;
+				case null =>
+					fmt::fprintf(ctx.current.output,
+						"#unresolved type {}#", t.name)?;
+					return;
+				};
+				match (lookup_type_in_namespace(ns, second)) {
+				case let id: str =>
+					emit_object(ctx, ns, id)?;
+				case =>
+					fmt::fprintf(ctx.current.output,
+						"#unresolved type {}#", t.name)?;
+				};
+			};
+		};
+	case let t: gir::array_type =>
+		if (t.fixed_size == 0) {
+			if (len(t.c_type) > 0) {
+				const parsed = parse_ctype(t.c_type);
+				defer ctype_finish(parsed);
+				return emit_c_type(ctx, parsed);
+			} else {
+				fmt::fprint(ctx.current.output, "*")?;
+				return emit_type(ctx, *t.inner)?;
+			};
+		} else {
+			fmt::fprintf(ctx.current.output, "[{}]", t.fixed_size)?;
+			return emit_type(ctx, *t.inner)?;
+		};
+	case let t: gir::callback =>
+		return emit_callback_type(ctx, &t);
+	};
+};
+
+fn emit_callback_type(ctx: *context, cb: *gir::callback) (void | io::error) = {
+	fmt::fprint(ctx.current.output, "*fn(")?;
+	emit_params(ctx, cb.params)?;
+	if (cb.throws) {
+		if (len(cb.params) > 0) {
+			fmt::fprint(ctx.current.output, ", ")?;
+		};
+		fmt::fprint(ctx.current.output, "error: nullable **")?;
+		emit_object(ctx, ctx.glib, "Error")?;
+	};
+	fmt::fprint(ctx.current.output, ") ")?;
+	emit_return_value(ctx, cb.return_value)?;
+};
+
+fn emit_c_type(ctx: *context, type_: ctype) (void | io::error) = {
+	match (type_) {
+	case let special: (cmodule, str) =>
+		const (mod, id) = special;
+		switch (mod) {
+		case cmodule::LIBC =>
+			add_import(ctx.current, ["types", "libc"]);
+			fmt::fprintf(ctx.current.output, "libc::{}", id)?;
+		case cmodule::GLIB =>
+			emit_object(ctx, ctx.glib, id)?;
+		case cmodule::GOBJECT =>
+			emit_object(ctx, ctx.gobject, id)?;
+		};
+	case let id: str =>
+		match (lookup_type(ctx, id)) {
+		case let pair: (*namespace, str) =>
+			emit_object(ctx, pair.0, pair.1)?;
+		case =>
+			fmt::fprintf(ctx.current.output, "#unresolved type {}#",
+				id)?;
+		};
+	case let b: cbuiltin =>
+		if (cbuiltin_needs_import(b)) {
+			add_import(ctx.current, ["types", "c"]);
+		};
+		fmt::fprint(ctx.current.output, cbuiltin_str(b))?;
+	case let p: cpointer =>
+		fmt::fprint(ctx.current.output, '*')?;
+		return emit_c_type(ctx, *p);
+	};
+};
+
+fn emit_object(
+	ctx: *context,
+	source: *namespace,
+	object: str,
+) (void | io::error) = {
+	if (ident_eq(source.module, ctx.current.module)) {
+		fmt::fprint(ctx.current.output, object)?;
+	} else {
+		add_import(ctx.current, source.module);
+		fmt::fprintf(ctx.current.output, "{}::{}",
+			source.module[len(source.module) - 1], object)?;
+	};
+};
+
+fn is_exported(ctx: *context, symbol: str) bool = {
+	for (let i = 0z; i < len(ctx.current.exports); i += 1) {
+		if (ctx.current.exports[i] == symbol) {
+			return true;
+		};
+	};
+	return false;
+};
diff --git a/cmd/hare-gi/ident.ha b/cmd/hare-gi/ident.ha
new file mode 100644
index 0000000..962a6b3
--- /dev/null
+++ b/cmd/hare-gi/ident.ha
@@ -0,0 +1,144 @@
+use ascii;
+use fmt;
+use io;
+use strings;
+use strio;
+
+const keywords: [_]str = [
+	"abort", "align", "alloc", "append", "as", "assert", "bool", "break",
+	"case", "const", "continue", "defer", "def", "delete", "else", "enum",
+	"export", "f32", "f64", "false", "fn", "for", "free", "i16", "i32",
+	"i64", "i8", "if", "insert", "int", "is", "len", "let", "match", "null",
+	"nullable", "offset", "return", "rune", "size", "static", "str",
+	"struct", "switch", "true", "type", "u16", "u32", "u64", "u8", "uint",
+	"uintptr", "union", "use", "vaarg", "vaend", "valist", "vastart",
+	"void", "yield",
+];
+
+// Makes an identifier suitable for use in Hare code.
+// If the identifier doesn't start with a letter or '_', an '_' is prepended.
+// If the identifier matches one of Hare's keywords, an '_' is appended.
+fn fix_identifier(s: str) str = {
+	static let buf: [128]u8 = [0...];
+
+	let iter = strings::iter(s);
+	const first = strings::next(&iter) as rune;
+	if (!ascii::isalpha(first) && first != '_') {
+		return fmt::bsprintf(buf, "_{}", s);
+	};
+
+	for (let i = 0z; i < len(keywords); i += 1) {
+		if (keywords[i] == s) {
+			return fmt::bsprintf(buf, "{}_", s);
+		};
+	};
+	return s;
+};
+
+// Normalizes a signal name into a form suitable as an identifier, replacing
+// '-' with '_'.
+fn normalize_signal(in: str) str = {
+	static let buf: [128]u8 = [0...];
+	let buf = strio::fixed(buf);
+
+	let iter = strings::iter(in);
+	for (true) match (strings::next(&iter)) {
+	case let r: rune =>
+		strio::appendrune(&buf, if (r == '-') '_' else r)!;
+	case =>
+		break;
+	};
+	return strio::string(&buf);
+};
+
+// Converts lower_case into UPPER_CASE
+fn to_uppercase(in: str) str = {
+	static let buf: [128]u8 = [0...];
+	let buf = strio::fixed(buf);
+
+	let iter = strings::iter(in);
+	for (true) match (strings::next(&iter)) {
+	case let r: rune =>
+		strio::appendrune(&buf, ascii::toupper(r))!;
+	case =>
+		break;
+	};
+	return strio::string(&buf);
+};
+
+// Converts PascalCase into snake_case
+fn swap_case(in: str) str = {
+	let parts: []str = [];
+	defer strings::freeall(parts);
+
+	let buf = strio::dynamic();
+	defer io::close(&buf)!;
+
+	let iter = strings::iter(in);
+	let prev = '!';
+	for (true) match (strings::next(&iter)) {
+	case let r: rune =>
+		if (ascii::isupper(r) && !ascii::isupper(prev)) {
+			if (len(strio::string(&buf)) > 0) {
+				append(parts,
+					strings::dup(strio::string(&buf)));
+				strio::reset(&buf);
+			};
+		};
+		strio::appendrune(&buf, ascii::tolower(r))!;
+		prev = r;
+	case void =>
+		break;
+	};
+
+	if (len(strio::string(&buf)) > 0) {
+		append(parts, strings::dup(strio::string(&buf)));
+	};
+
+	return strings::join("_", parts...);
+};
+
+@test fn swap_case() void = {
+	const tests = [
+		("Gtk", "gtk"),
+		("GtkActionBar", "gtk_action_bar"),
+		("GtkWidget", "gtk_widget"),
+		("DBus", "dbus"),
+		("cairo_t", "cairo_t"),
+	];
+	for (let i = 0z; i < len(tests); i += 1) {
+		const (in, expected) = tests[i];
+		const out = swap_case(in);
+		if (out != expected) {
+			fmt::fatalf("Fail '{}': expected '{}', got '{}'",
+				in, expected, out);
+		};
+		free(out);
+	};
+};
+
+// A fully quantified identifier.
+type ident = []str;
+
+// Returns whether two fully quantified identifiers match.
+fn ident_eq(a: ident, b: ident) bool = {
+	if (len(a) != len(b)) {
+		return false;
+	};
+	for (let i = 0z; i < len(a); i += 1) {
+		if (a[i] != b[i]) {
+			return false;
+		};
+	};
+	return true;
+};
+
+// Writes a fully quantified identifier to an [[io::handle]].
+fn emit_ident(out: io::handle, id: ident) (void | io::error) = {
+	for (let i = 0z; i < len(id); i += 1) {
+		if (i > 0) {
+			fmt::fprint(out, "::")?;
+		};
+		fmt::fprint(out, id[i])?;
+	};
+};
diff --git a/cmd/hare-gi/main.ha b/cmd/hare-gi/main.ha
new file mode 100644
index 0000000..6aa1213
--- /dev/null
+++ b/cmd/hare-gi/main.ha
@@ -0,0 +1,148 @@
+use fmt;
+use fs;
+use getopt;
+use gir;
+use io;
+use os;
+use strings;
+
+export fn main() void = {
+	const help: [_]getopt::help = [
+		"generate Hare bindings from GObject-Introspection files",
+		('c', "verify the files instead of generating code"),
+		('B', "disable documentation comments"),
+		('m', "namespace,<module>,<output", "register a module binding namespace <namespace> to Hare module <module>. If provided, also write the code to <output>"),
+		"files...",
+	];
+	const cmd = getopt::parse(os::args, help...);
+	defer getopt::finish(&cmd);
+
+	let ctx = context {
+		glib = null: *namespace,
+		gobject = null: *namespace,
+		...
+	};
+	defer context_finish(&ctx);
+
+	let check = false;
+	for (let i = 0z; i < len(cmd.opts); i += 1) {
+		const (opt, _) = cmd.opts[i];
+		switch (opt) {
+		case 'c' =>
+			check = true;
+		case 'B' =>
+			ctx.brief = true;
+		case 'm' =>
+			yield; // parsed later
+		case => abort();
+		};
+	};
+
+	for (let i = 0z; i < len(cmd.args); i += 1) {
+		const f = match (os::open(cmd.args[i])) {
+		case let f: io::file =>
+			yield f;
+		case let err: fs::error =>
+			fmt::fatalf("Failed to open {}: {}",
+				cmd.args[i], fs::strerror(err));
+		};
+		defer io::close(f)!;
+
+		match (gir::parse(f)) {
+		case let r: gir::repository =>
+			add_repository(&ctx, r);
+		case let err: gir::error =>
+			fmt::fatalf("Failed to parse {}: {}",
+				cmd.args[i], gir::strerror(err));
+		};
+	};
+
+	for (let i = 0z; i < len(cmd.opts); i += 1) {
+		const (opt, arg) = cmd.opts[i];
+		switch (opt) {
+		case 'm' =>
+			const (ns, rest) = strings::cut(arg, ",");
+			const (module, output) = strings::cut(rest, ",");
+
+			if (len(ns) == 0) {
+				fmt::fatalf("Invalid -o {}: empty namespace",
+					arg);
+			};
+			if (len(module) == 0) {
+				fmt::fatalf("Invalid -o {}: empty module", arg);
+			};
+
+			const ns = match (get_namespace(&ctx, ns)) {
+			case let ns: *namespace =>
+				yield ns;
+			case null =>
+				fmt::fatalf("No such namespace '{}'", ns);
+			};
+			ns.module = strings::split(module, "::");
+			if (len(output) > 0) {
+				match (os::create(output, fs::mode::USER_RW)) {
+				case let f: io::file =>
+					ns.output_file = f;
+				case let err: fs::error =>
+					fmt::fatalf("Failed to open {}: {}",
+						output, fs::strerror(err));
+				};
+			};
+		case =>
+			yield;
+		};
+	};
+
+	let errors = 0z;
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		const ns = &ctx.namespaces[i];
+		if (len(ns.module) == 0) {
+			fmt::errorln("A module was not provided for namespace",
+				ns.name)!;
+			errors += 1;
+		};
+		for (let j = 0z; j < len(ns.includes); j += 1) {
+			const incl = &ns.includes[j];
+			match (get_namespace(&ctx, incl.name, incl.version)) {
+			case *namespace => yield;
+			case null =>
+				fmt::errorfln(
+					"Missing namespace {} (version {}) "
+					"required by {}",
+					incl.name, incl.version, ns.name)!;
+				errors += 1;
+			};
+		};
+	};
+	if (errors > 0) {
+		os::exit(1);
+	};
+
+	if (check) {
+		return;
+	};
+
+	match (get_namespace(&ctx, "GLib")) {
+	case let ns: *namespace =>
+		ctx.glib = ns;
+	case null =>
+		fmt::fatal("GLib namespace is required but was not provided");
+	};
+
+	match (get_namespace(&ctx, "GObject")) {
+	case let ns: *namespace =>
+		ctx.gobject = ns;
+	case null =>
+		fmt::fatal("GObject namespace is required but was not provided");
+	};
+
+	populate(&ctx);
+
+	emit_empty(&ctx);
+
+	match (emit(&ctx)) {
+	case void => yield;
+	case let err: io::error =>
+		fmt::fatal("Error:", io::strerror(err));
+	};
+};
diff --git a/cmd/hare-gi/populate.ha b/cmd/hare-gi/populate.ha
new file mode 100644
index 0000000..31e663b
--- /dev/null
+++ b/cmd/hare-gi/populate.ha
@@ -0,0 +1,62 @@
+use gir;
+use strings;
+
+// First stage of codegen: enumerates all types and builds an identifier table,
+// which can be then used to get a (namespace, str) pair for a given C
+// identifier. See [[lookup_type]].
+fn populate(ctx: *context) void = {
+	for (let i = 0z; i < len(ctx.namespaces); i += 1) {
+		populate_namespace(ctx, &ctx.namespaces[i]);
+	};
+};
+
+fn populate_namespace(ctx: *context, ns: *namespace) void = {
+	ctx.current = ns;
+
+	for (let i = 0z; i < len(ns.aliases); i += 1) {
+		const alias = &ns.aliases[i];
+		populate_type(ctx, alias.c_type, alias.name);
+	};
+	for (let i = 0z; i < len(ns.classes); i += 1) {
+		const class = &ns.classes[i];
+		if (len(class.c_type) > 0) {
+			populate_type(ctx, class.c_type, class.name);
+		} else {
+			populate_type(ctx, class.glib_type_name, class.name);
+		};
+	};
+	for (let i = 0z; i < len(ns.interfaces); i += 1) {
+		const iface = &ns.interfaces[i];
+		populate_type(ctx, iface.c_type, iface.name);
+	};
+	for (let i = 0z; i < len(ns.records); i += 1) {
+		const record = &ns.records[i];
+		populate_type(ctx, record.c_type, record.name);
+	};
+	for (let i = 0z; i < len(ns.enums); i += 1) {
+		const enumeration = &ns.enums[i];
+		populate_type(ctx, enumeration.c_type, enumeration.name);
+	};
+	for (let i = 0z; i < len(ns.unions); i += 1) {
+		const union_ = &ns.unions[i];
+		populate_type(ctx, union_.c_type, union_.name);
+	};
+	for (let i = 0z; i < len(ns.bitfields); i += 1) {
+		const bitfield = &ns.bitfields[i];
+		populate_type(ctx, bitfield.c_type, bitfield.name);
+	};
+	for (let i = 0z; i < len(ns.callbacks); i += 1) {
+		const callback = &ns.callbacks[i];
+		if (len(callback.c_type) > 0) {
+			populate_type(ctx, callback.c_type, callback.name);
+		};
+	};
+};
+
+fn populate_type(ctx: *context, c: str, name: str) void = {
+	if (len(c) == 0) {
+		return;
+	};
+	const hare = strings::dup(fix_identifier(name));
+	append(ctx.current.types, (c, hare));
+};
diff --git a/cmd/xmltree/main.ha b/cmd/xmltree/main.ha
new file mode 100644
index 0000000..5f15eaa
--- /dev/null
+++ b/cmd/xmltree/main.ha
@@ -0,0 +1,104 @@
+use xml = format::fastxml;
+use sort;
+use os;
+use fmt;
+use strings;
+
+type node = struct {
+	name: str,
+	attributes: []str,
+	has_text: bool,
+
+	children: []*node,
+};
+
+fn add_attr(n: *node, attr: xml::attribute) void = {
+	for (let i = 0z; i < len(n.attributes); i += 1) {
+		if (n.attributes[i] == attr.0) return;
+	};
+	append(n.attributes, strings::dup(attr.0));
+};
+
+fn add_text(n: *node, text: str) void = {
+	const clean = strings::trim(text);
+	n.has_text ||= len(clean) > 0;
+};
+
+fn get_node(n: *node, name: str) *node = {
+	for (let i = 0z; i < len(n.children); i += 1) {
+		if (n.children[i].name == name)
+			return n.children[i];
+	};
+	let new = alloc(node { name = strings::dup(name), ... });
+	append(n.children, new);
+	return new;
+};
+
+fn print(n: *node, indent: size) void = {
+	for (let i = 0z; i < indent; i += 1) fmt::print("> ")!;
+
+	fmt::print(n.name)!;
+	if (n.has_text) fmt::print("+")!;
+	fmt::print(" [")!;
+	for (let i = 0z; i < len(n.attributes); i += 1) {
+		fmt::printf("{}{}", if (i == 0) "" else " ", n.attributes[i])!;
+	};
+	fmt::println("]")!;
+
+	for (let i = 0z; i < len(n.children); i += 1) {
+		print(n.children[i], indent + 1);
+	};
+};
+
+export fn main() void = {
+	if (len(os::args) - 1 < 1) {
+		fmt::fatal("Usage: xmltree <file.xml>");
+	};
+
+	const file = os::open(os::args[1])!;
+	const parser = xml::parse(file)!;
+	defer xml::parser_free(parser);
+
+	let tree = null: *node;
+	defer free(tree); // memory leaks!
+
+	let stack: []*node = [];
+	defer free(stack);
+
+	for (true) {
+		const token = match (xml::scan(parser)!) {
+		case let t: xml::token =>
+			yield t;
+		case void =>
+			break;
+		};
+
+		match (token) {
+		case let start: xml::elementstart =>
+			if (len(stack) == 0) {
+				tree = alloc(node {
+					name = strings::dup(start),
+					...
+				});
+				append(stack, tree);
+			} else {
+				let current = stack[len(stack) - 1];
+				append(stack, get_node(current, start));
+			};
+		case let end: xml::elementend =>
+			//fmt::printfln("stack => {}", stack[len(stack) - 1].name)!;
+			//fmt::printfln("file => {}", end)!;
+			assert(end == stack[len(stack) - 1].name);
+			delete(stack[len(stack) - 1]);
+			if (len(stack) == 0) break;
+		case let attr: xml::attribute =>
+			let current = stack[len(stack) - 1];
+			add_attr(current, attr);
+		case let text: xml::text =>
+			let current = stack[len(stack) - 1];
+			add_text(current, text);
+		};
+	};
+
+	print(tree, 0);
+};