about summary refs log blame commit diff stats
path: root/apps/calls.subx
blob: 4b91b6284ef303c30b75a6b18fa8abe91c1e2a8f (plain) (tree)
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
/* $xxxterm$ */
/*
 * Copyright (c) 2010 Marco Peereboom <marco@peereboom.us>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * TODO:
 *	inverse color browsing
 *	favs
 *	download files status
 *	multi letter commands
 *	pre and post counts for commands
 *	search on page
 *	search on engines
 *	fav icon
 *	close tab X
 *	autocompletion on various inputs
 */

#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <util.h>

#include <sys/queue.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <webkit/webkit.h>
#include <libsoup/soup.h>
#include <JavaScriptCore/JavaScript.h>

static char		*version = "$xxxterm$";

#define XT_DEBUG
/* #define XT_DEBUG */
#ifdef XT_DEBUG
#define DPRINTF(x...)		do { if (swm_debug) fprintf(stderr, x); } while (0)
#define DNPRINTF(n,x...)	do { if (swm_debug & n) fprintf(stderr, x); } while (0)
#define	XT_D_MOVE		0x0001
#define	XT_D_KEY		0x0002
#define	XT_D_TAB		0x0004
#define	XT_D_URL		0x0008
#define	XT_D_CMD		0x0010
#define	XT_D_NAV		0x0020
#define	XT_D_DOWNLOAD		0x0040
#define	XT_D_CONFIG		0x0080
u_int32_t		swm_debug = 0
			    | XT_D_MOVE
			    | XT_D_KEY
			    | XT_D_TAB
			    | XT_D_URL
			    | XT_D_CMD
			    | XT_D_NAV
			    | XT_D_DOWNLOAD
			    | XT_D_CONFIG
			    ;
#else
#define DPRINTF(x...)
#define DNPRINTF(n,x...)
#endif

#define LENGTH(x)		(sizeof x / sizeof x[0])
#define CLEAN(mask)		(mask & ~(GDK_MOD2_MASK) &	\
				    ~(GDK_BUTTON1_MASK) &	\
				    ~(GDK_BUTTON2_MASK) &	\
				    ~(GDK_BUTTON3_MASK) &	\
				    ~(GDK_BUTTON4_MASK) &	\
				    ~(GDK_BUTTON5_MASK))

struct tab {
	TAILQ_ENTRY(tab)	entry;
	GtkWidget		*vbox;
	GtkWidget		*label;
	GtkWidget		*uri_entry;
	GtkWidget		*toolbar;
	GtkWidget		*browser_win;
	GtkWidget		*cmd;
	guint			tab_id;

	/* adjustments for browser */
	GtkScrollbar		*sb_h;
	GtkScrollbar		*sb_v;
	GtkAdjustment		*adjust_h;
	GtkAdjustment		*adjust_v;

	/* flags */
	int			focus_wv;
	int			ctrl_click;
	gchar			*hover;

	WebKitWebView		*wv;
	WebKitWebSettings	*settings;
};
TAILQ_HEAD(tab_list, tab);

struct karg {
	int		i;
	char		*s;
};

/* defines */
#define XT_DIR			(".xxxterm")
#define XT_CONF_FILE		("xxxterm.conf")
#define XT_CB_HANDLED		(TRUE)
#define XT_CB_PASSTHROUGH	(FALSE)

/* actions */
#define XT_MOVE_INVALID		(0)
#define XT_MOVE_DOWN		(1)
#define XT_MOVE_UP		(2)
#define XT_MOVE_BOTTOM		(3)
#define XT_MOVE_TOP		(4)
#define XT_MOVE_PAGEDOWN	(5)
#define XT_MOVE_PAGEUP		(6)
#define XT_MOVE_LEFT		(7)
#define XT_MOVE_FARLEFT		(8)
#define XT_MOVE_RIGHT		(9)
#define XT_MOVE_FARRIGHT	(10)

#define XT_TAB_PREV		(-2)
#define XT_TAB_NEXT		(-1)
#define XT_TAB_INVALID		(0)
#define XT_TAB_NEW		(1)
#define XT_TAB_DELETE		(2)
#define XT_TAB_DELQUIT		(3)
#define XT_TAB_OPEN		(4)

#define XT_NAV_INVALID		(0)
#define XT_NAV_BACK		(1)
#define XT_NAV_FORWARD		(2)

/* globals */
extern char		*__progname;
struct passwd		*pwd;
GtkWidget		*main_window;
GtkNotebook		*notebook;
struct tab_list		tabs;

/* settings */
int			showtabs = 1;	/* show tabs on notebook */
int			showurl = 1;	/* show url toolbar on notebook */
int			tabless = 0;	/* allow only 1 tab */
int			ctrl_click_focus = 0; /* ctrl click gets focus */
int			cookies_enabled = 1; /* enable cookies */
int			read_only_cookies = 0; /* enable to not write cookies */
int			enable_scripts = 1;
int			enable_plugins = 1;
int			default_font_size = 12;

char			*home = "http://www.peereboom.us";
char			*http_proxy = NULL;
SoupURI			*proxy_uri = NULL;
char			work_dir[PATH_MAX];
char			cookie_file[PATH_MAX];
char			download_dir[PATH_MAX];
SoupSession		*session;
SoupCookieJar		*cookiejar;

/* protos */
void			create_new_tab(char *, int);
void			delete_tab(struct tab *);

struct valid_url_types {
	char		*type;
} vut[] = {
	{ "http://" },
	{ "https://" },
	{ "ftp://" },
	{ "file://" },
};

int
valid_url_type(char *url)
{
	int			i;

	for (i = 0; i < LENGTH(vut); i++)
		if (!strncasecmp(vut[i].type, url, strlen(vut[i].type)))
			return (0);

	return (1);
}

char *
guess_url_type(char *url_in)
{
	struct stat		sb;
	char			*url_out = NULL;

	/* XXX not sure about this heuristic */
	if (stat(url_in, &sb) == 0) {
		if (asprintf(&url_out, "file://%s", url_in) == -1)
			err(1, "aprintf file");
	} else {
		/* guess http */
		if (asprintf(&url_out, "http://%s", url_in) == -1)
			err(1, "aprintf http");
	}

	if (url_out == NULL)
		err(1, "asprintf pointer");

	DNPRINTF(XT_D_URL, "guess_url_type: guessed %s\n", url_out);

	return (url_out);
}

#define	WS	"\n= \t"
void
config_parse(char *filename)
{
	FILE			*config;
	char			*line, *cp, *var, *val;
	size_t			 len, lineno = 0;

	DNPRINTF(XT_D_CONFIG, "config_parse: filename %s\n", filename);

	if (filename == NULL)
		return;

	if ((config = fopen(filename, "r")) == NULL) {
		warn("config_parse: cannot open %s", filename);
		return;
	}

	for (;;) {
		if ((line = fparseln(config, &len, &lineno, NULL, 0)) == NULL)
			if (feof(config))
				break;

		cp = line;
		cp += (long)strspn(cp, WS);
		if (cp[0] == '\0') {
			/* empty line */
			free(line);
			continue;
		}

		if ((var = strsep(&cp, WS)) == NULL || cp == NULL)
			break;

		cp += (long)strspn(cp, WS);
		if ((val = strsep(&cp, WS)) == NULL)
			break;

		DNPRINTF(XT_D_CONFIG, "config_parse: %s=%s\n",var ,val);

		/* get settings */
		if (!strcmp(var, "home"))
			home = strdup(val);
		else if (!strcmp(var, "ctrl_click_focus"))
			ctrl_click_focus = atoi(val);
		else if (!strcmp(var, "read_only_cookies"))
			read_only_cookies = atoi(val);
		else if (!strcmp(var, "cookies_enabled"))
			cookies_enabled = atoi(val);
		else if (!strcmp(var, "enable_scripts"))
			enable_scripts = atoi(val);
		else if (!strcmp(var, "enable_plugins"))
			enable_plugins = atoi(val);
		else if (!strcmp(var, "default_font_size"))
			default_font_size = atoi(val);
		else if (!strcmp(var, "http_proxy")) {
			http_proxy = strdup(val);
			if (http_proxy == NULL)
				err(1, "http_proxy");
		} else if (!strcmp(var, "download_dir")) {
			if (val[0] == '~')
				snprintf(download_dir, sizeof download_dir,
				    "%s/%s", pwd->pw_dir, &val[1]);
			else
				strlcpy(download_dir, val, sizeof download_dir);
			fprintf(stderr, "download dir: %s\n", download_dir);
		} else
			errx(1, "invalid conf file entry: %s=%s", var, val);

		free(line);
	}

	fclose(config);
}
int
quit(struct tab *t, struct karg *args)
{
	gtk_main_quit();

	return (1);
}

int
help(struct tab *t, struct karg *args)
{
	if (t == NULL)
		errx(1, "help");

	webkit_web_view_load_string(t->wv,
	    "<html><body><h1>XXXTerm</h1></body></html>",
	    NULL,
	    NULL,
	    NULL);

	return (0);
}

int
navaction(struct tab *t, struct karg *args)
{
	DNPRINTF(XT_D_NAV, "navaction: tab %d opcode %d\n",
	    t->tab_id, args->i);

	switch (args->i) {
	case XT_NAV_BACK:
		webkit_web_view_go_back(t->wv);
		break;
	case XT_NAV_FORWARD:
		webkit_web_view_go_forward(t->wv);
		break;
	}
	return (XT_CB_PASSTHROUGH);
}

int
move(struct tab *t, struct karg *args)
{
	GtkAdjustment		*adjust;
	double			pi, si, pos, ps, upper, lower, max;

	switch (args->i) {
	case XT_MOVE_DOWN:
	case XT_MOVE_UP:
	case XT_MOVE_BOTTOM:
	case XT_MOVE_TOP:
	case XT_MOVE_PAGEDOWN:
	case XT_MOVE_PAGEUP:
		adjust = t->adjust_v;
		break;
	default:
		adjust = t->adjust_h;
		break;
	}

	pos = gtk_adjustment_get_value(adjust);
	ps = gtk_adjustment_get_page_size(adjust);
	upper = gtk_adjustment_get_upper(adjust);
	lower = gtk_adjustment_get_lower(adjust);
	si = gtk_adjustment_get_step_increment(adjust);
	pi = gtk_adjustment_get_page_increment(adjust);
	max = upper - ps;

	DNPRINTF(XT_D_MOVE, "move: opcode %d %s pos %f ps %f upper %f lower %f "
	    "max %f si %f pi %f\n",
	    args->i, adjust == t->adjust_h ? "horizontal" : "vertical", 
	    pos, ps, upper, lower, max, si, pi);

	switch (args->i) {
	case XT_MOVE_DOWN:
	case XT_MOVE_RIGHT:
		pos += si;
		gtk_adjustment_set_value(adjust, MIN(pos, max));
		break;
	case XT_MOVE_UP:
	case XT_MOVE_LEFT:
		pos -= si;
		gtk_adjustment_set_value(adjust, MAX(pos, lower));
		break;
	case XT_MOVE_BOTTOM:
	case XT_MOVE_FARRIGHT:
		gtk_adjustment_set_value(adjust, max);
		break;
	case XT_MOVE_TOP:
	case XT_MOVE_FARLEFT:
		gtk_adjustment_set_value(adjust, lower);
		break;
	case XT_MOVE_PAGEDOWN:
		pos += pi;
		gtk_adjustment_set_value(adjust, MIN(pos, max));
		break;
	case XT_MOVE_PAGEUP:
		pos -= pi;
		gtk_adjustment_set_value(adjust, MAX(pos, lower));
		break;
	default:
		return (XT_CB_PASSTHROUGH);
	}

	DNPRINTF(XT_D_MOVE, "move: new pos %f %f\n", pos, MIN(pos, max));

	return (XT_CB_HANDLED);
}

char *
getparams(char *cmd, char *cmp)
{
	char			*rv = NULL;

	if (cmd && cmp) {
		if (!strncmp(cmd, cmp, strlen(cmp))) {
			rv = cmd + strlen(cmp);
			while (*rv == ' ')
				rv++;
			if (strlen(rv) == 0)
				rv = NULL;
		}
	}

	return (rv);
}

int
tabaction(struct tab *t, struct karg *args)
{
	int			rv = XT_CB_HANDLED;
	char			*url = NULL, *newuri = NULL;

	DNPRINTF(XT_D_TAB, "tabaction: %p %d %d\n", t, args->i, t->focus_wv);

	if (t == NULL)
		return (XT_CB_PASSTHROUGH);

	switch (args->i) {
	case XT_TAB_NEW:
		if ((url = getparams(args->s, "tabnew")))
			create_new_tab(url, 1);
		else
			create_new_tab(NULL, 1);
		break;
	case XT_TAB_DELETE:
		delete_tab(t);
		break;
	case XT_TAB_DELQUIT:
		if (gtk_notebook_get_n_pages(notebook) > 1)
			delete_tab(t);
		else
			quit(t, args);
		break;
	case XT_TAB_OPEN:
		if ((url = getparams(args->s, "open")) ||
		    ((url = getparams(args->s, "op"))) ||
		    ((url = getparams(args->s, "o"))))
			;
		else {
			rv = XT_CB_PASSTHROUGH;
			goto done;
		}

		if (valid_url_type(url)) {
			newuri = guess_url_type(url);
			url = newuri;
		}
		webkit_web_view_load_uri(t->wv, url);
		if (newuri)
			free(newuri);
		break;
	default:
		rv = XT_CB_PASSTHROUGH;
		goto done;
	}

done:
	if (args->s) {
		free(args->s);
		args->s = NULL;
	}

	return (rv);
}

int
movetab(struct tab *t, struct karg *args)
{
	struct tab		*tt;
	int			x;

	DNPRINTF(XT_D_TAB, "movetab: %p %d\n", t, args->i);

	if (t == NULL)
		return (XT_CB_PASSTHROUGH);

	if (args->i == XT_TAB_INVALID)
		return (XT_CB_PASSTHROUGH);

	if (args->i < XT_TAB_INVALID) {
		/* next or previous tab */
		if (TAILQ_EMPTY(&tabs))
			return (XT_CB_PASSTHROUGH);

		if (args->i == XT_TAB_NEXT)
			gtk_notebook_next_page(notebook);
		else
			gtk_notebook_prev_page(notebook);

		return (XT_CB_HANDLED);
	}

	/* jump to tab */
	x = args->i - 1;
	if (t->tab_id == x) {
		DNPRINTF(XT_D_TAB, "movetab: do nothing\n");
		return (XT_CB_HANDLED);
	}

	TAILQ_FOREACH(tt, &tabs, entry) {
		if (tt->tab_id == x) {
			gtk_notebook_set_current_page(notebook, x);
			DNPRINTF(XT_D_TAB, "movetab: going to %d\n", x);
			if (tt->focus_wv)
				gtk_widget_grab_focus(GTK_WIDGET(tt->wv));
		}
	}

	return (XT_CB_HANDLED);
}

int
command(struct tab *t, struct karg *args)
{
	DNPRINTF(XT_D_CMD, "command:\n");

	gtk_entry_set_text(GTK_ENTRY(t->cmd), ":");
	gtk_widget_show(t->cmd);
	gtk_widget_grab_focus(GTK_WIDGET(t->cmd));
	gtk_editable_set_position(GTK_EDITABLE(t->cmd), -1);

	return (XT_CB_HANDLED);
}

/* inherent to GTK not all keys will be caught at all times */
struct key {
	guint		mask;
	guint		modkey;
	guint		key;
	int		(*func)(struct tab *, struct karg *);
	struct karg	arg;
} keys[] = {
	{ GDK_SHIFT_MASK,	0,	GDK_colon,	command,	{0} },
	{ GDK_CONTROL_MASK,	0,	GDK_q,		quit,		{0} },

	/* navigation */
	{ 0,			0,	GDK_BackSpace,	navaction,	{.i = XT_NAV_BACK} },
	{ GDK_MOD1_MASK,	0,	GDK_Left,	navaction,	{.i = XT_NAV_BACK} },
	{ GDK_SHIFT_MASK,	0,	GDK_BackSpace,	navaction,	{.i = XT_NAV_FORWARD} },
	{ GDK_MOD1_MASK,	0,	GDK_Right,	navaction,	{.i = XT_NAV_FORWARD} },

	/* vertical movement */
	{ 0,			0,	GDK_j,		move,		{.i = XT_MOVE_DOWN} },
	{ 0,			0,	GDK_Down,	move,		{.i = XT_MOVE_DOWN} },
	{ 0,			0,	GDK_Up,		move,		{.i = XT_MOVE_UP} },
	{ 0,			0,	GDK_k,		move,		{.i = XT_MOVE_UP} },
	{ GDK_SHIFT_MASK,	0,	GDK_G,		move,		{.i = XT_MOVE_BOTTOM} },
	{ 0,			0,	GDK_End,	move,		{.i = XT_MOVE_BOTTOM} },
	{ 0,			0,	GDK_Home,	move,		{.i = XT_MOVE_TOP} },
	{ 0,			GDK_g,	GDK_g,		move,		{.i = XT_MOVE_TOP} }, /* XXX make this work */
	{ 0,			0,	GDK_space,	move,		{.i = XT_MOVE_PAGEDOWN} },
	{ 0,			0,	GDK_Page_Down,	move,		{.i = XT_MOVE_PAGEDOWN} },
	{ 0,			0,	GDK_Page_Up,	move,		{.i = XT_MOVE_PAGEUP} },
	/* horizontal movement */
	{ 0,			0,	GDK_l,		move,		{.i = XT_MOVE_RIGHT} },
	{ 0,			0,	GDK_Right,	move,		{.i = XT_MOVE_RIGHT} },
	{ 0,			0,	GDK_Left,	move,		{.i = XT_MOVE_LEFT} },
	{ 0,			0,	GDK_h,		move,		{.i = XT_MOVE_LEFT} },
	{ GDK_SHIFT_MASK,	0,	GDK_dollar,	move,		{.i = XT_MOVE_FARRIGHT} },
	{ 0,			0,	GDK_0,		move,		{.i = XT_MOVE_FARLEFT} },

	/* tabs */
	{ GDK_CONTROL_MASK,	0,	GDK_t,		tabaction,	{.i = XT_TAB_NEW} },
	{ GDK_CONTROL_MASK,	0,	GDK_w,		tabaction,	{.i = XT_TAB_DELETE} },
	{ GDK_CONTROL_MASK,	0,	GDK_1,		movetab,	{.i = 1} },
	{ GDK_CONTROL_MASK,	0,	GDK_2,		movetab,	{.i = 2} },
	{ GDK_CONTROL_MASK,	0,	GDK_3,		movetab,	{.i = 3} },
	{ GDK_CONTROL_MASK,	0,	GDK_4,		movetab,	{.i = 4} },
	{ GDK_CONTROL_MASK,	0,	GDK_5,		movetab,	{.i = 5} },
	{ GDK_CONTROL_MASK,	0,	GDK_6,		movetab,	{.i = 6} },
	{ GDK_CONTROL_MASK,	0,	GDK_7,		movetab,	{.i = 7} },
	{ GDK_CONTROL_MASK,	0,	GDK_8,		movetab,	{.i = 8} },
	{ GDK_CONTROL_MASK,	0,	GDK_9,		movetab,	{.i = 9} },
	{ GDK_CONTROL_MASK,	0,	GDK_0,		movetab,	{.i = 10} },
};

struct cmd {
	char		*cmd;
	int		params;
	int		(*func)(struct tab *, struct karg *);
	struct karg	arg;
} cmds[] = {
	{ "q!",			0,	quit,			{0} },
	{ "qa",			0,	quit,			{0} },
	{ "qa!",		0,	quit,			{0} },
	{ "help",		0,	help,			{0} },

	/* tabs */
	{ "o",			1,	tabaction,		{.i = XT_TAB_OPEN} },
	{ "op",			1,	tabaction,		{.i = XT_TAB_OPEN} },
	{ "open",		1,	tabaction,		{.i = XT_TAB_OPEN} },
	{ "tabnew",		1,	tabaction,		{.i = XT_TAB_NEW} },
	{ "tabedit",		0,	tabaction,		{.i = XT_TAB_NEW} },
	{ "tabe",		0,	tabaction,		{.i = XT_TAB_NEW} },
	{ "tabclose",		0,	tabaction,		{.i = XT_TAB_DELETE} },
	{ "quit",		0,	tabaction,		{.i = XT_TAB_DELQUIT} },
	{ "q",			0,	tabaction,		{.i = XT_TAB_DELQUIT} },
	/* XXX add count to these commands and add tabl and friends */
	{ "tabprevious",	0,	movetab,		{.i = XT_TAB_PREV} },
	{ "tabp",		0,	movetab,		{.i = XT_TAB_PREV} },
	{ "tabnext",		0,	movetab,		{.i = XT_TAB_NEXT} },
	{ "tabn",		0,	movetab,		{.i = XT_TAB_NEXT} },
};

void
focus_uri_entry_cb(GtkWidget* w, GtkDirectionType direction, struct tab *t)
{
	DNPRINTF(XT_D_URL, "focus_uri_entry_cb: tab %d focus_wv %d\n",
	    t->tab_id, t->focus_wv);

	if (t == NULL)
		errx(1, "focus_uri_entry_cb");

	/* focus on wv instead */
	if (t->focus_wv)
		gtk_widget_grab_focus(GTK_WIDGET(t->wv));
}

void
activate_uri_entry_cb(GtkWidget* entry, struct tab *t)
{
	const gchar		*uri = gtk_entry_get_text(GTK_ENTRY(entry));
	char			*newuri = NULL;

	DNPRINTF(XT_D_URL, "activate_uri_entry_cb: %s\n", uri);

	if (t == NULL)
		errx(1, "activate_uri_entry_cb");

	if (uri == NULL)
		errx(1, "uri");

	if (valid_url_type((char *)uri)) {
		newuri = guess_url_type((char *)uri);
		uri = newuri;
	}

	webkit_web_view_load_uri(t->wv, uri);
	gtk_widget_grab_focus(GTK_WIDGET(t->wv));

	if (newuri)
		free(newuri);
}

void
notify_load_status_cb(WebKitWebView* wview, GParamSpec* pspec, struct tab *t)
{
	WebKitWebFrame		*frame;
	const gchar		*uri;

	if (t == NULL)
		errx(1, "notify_load_status_cb");

	switch (webkit_web_view_get_load_status(wview)) {
	case WEBKIT_LOAD_COMMITTED:
		frame = webkit_web_view_get_main_frame(wview);
		uri = webkit_web_frame_get_uri(frame);
		if (uri)
			gtk_entry_set_text(GTK_ENTRY(t->uri_entry), uri);
		t->focus_wv = 1;

		/* take focus if we are visible */
		if (gtk_notebook_get_current_page(notebook) == t->tab_id)
			gtk_widget_grab_focus(GTK_WIDGET(t->wv));
		break;
	case WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT:
		uri = webkit_web_view_get_title(wview);
		if (uri == NULL) {
			frame = webkit_web_view_get_main_frame(wview);
			uri = webkit_web_frame_get_uri(frame);
		}
		gtk_label_set_text(GTK_LABEL(t->label), uri);
		break;
	case WEBKIT_LOAD_PROVISIONAL:
	case WEBKIT_LOAD_FINISHED:
	case WEBKIT_LOAD_FAILED:
	default:
		break;
	}
}

int
webview_npd_cb(WebKitWebView *wv, WebKitWebFrame *wf,
    WebKitNetworkRequest *request, WebKitWebNavigationAction *na,
    WebKitWebPolicyDecision *pd, struct tab *t)
{
	char			*uri;

	if (t == NULL)
		errx(1, "webview_npd_cb");

	DNPRINTF(XT_D_NAV, "webview_npd_cb: %s\n",
	    webkit_network_request_get_uri(request));

	if (t->ctrl_click) {
		uri = (char *)webkit_network_request_get_uri(request);
		create_new_tab(uri, ctrl_click_focus);
		t->ctrl_click = 0;
		webkit_web_policy_decision_ignore(pd);

		return (TRUE); /* we made the decission */
	}

	return (FALSE);
}

int
webview_event_cb(GtkWidget *w, GdkEventButton *e, struct tab *t)
{
	/* we can not eat the event without throwing gtk off so defer it */

	/* catch ctrl click */
	if (e->type == GDK_BUTTON_RELEASE && 
	    CLEAN(e->state) == GDK_CONTROL_MASK)
		t->ctrl_click = 1;
	else
		t->ctrl_click = 0;

	return (XT_CB_PASSTHROUGH);
}

int
webview_mimetype_cb(WebKitWebView *wv, WebKitWebFrame *frame,
    WebKitNetworkRequest *request, char *mime_type,
    WebKitWebPolicyDecision *decision, struct tab *t)
{
	if (t == NULL)
		errx(1, "webview_mimetype_cb");

	DNPRINTF(XT_D_DOWNLOAD, "webview_mimetype_cb: tab %d mime %s\n",
	    t->tab_id, mime_type);

	if (webkit_web_view_can_show_mime_type(wv, mime_type) == FALSE) {
		webkit_web_policy_decision_download(decision);
		return (TRUE);
	}

	return (FALSE);
}

int
webview_download_cb(WebKitWebView *wv, WebKitDownload *download, struct tab *t)
{
	const gchar		*filename;
	char			*uri = NULL;

	if (download == NULL || t == NULL)
		errx(1, "webview_download_cb: invalid pointers");

	filename = webkit_download_get_suggested_filename(download);
	if (filename == NULL)
		return (FALSE); /* abort download */

	if (asprintf(&uri, "file://%s/%s", download_dir, filename) == -1)
		err(1, "aprintf uri");

	DNPRINTF(XT_D_DOWNLOAD, "webview_download_cb: tab %d filename %s "
	    "local %s\n",
	    t->tab_id, filename, uri);

	webkit_download_set_destination_uri(download, uri);

	if (uri)
		free(uri);

	webkit_download_start(download);

	return (TRUE); /* start download */
}

void
webview_hover_cb(WebKitWebView *wv, gchar *title, gchar *uri, struct tab *t)
{
	DNPRINTF(XT_D_KEY, "webview_hover_cb: %s %s\n", title, uri);

	if (t == NULL)
		errx(1, "webview_hover_cb");

	if (uri) {
		if (t->hover) {
			free(t->hover);
			t->hover = NULL;
		}
		t->hover = strdup(uri);
	} else if (t->hover) {
		free(t->hover);
		t->hover = NULL;
	}
}

int
webview_keypress_cb(GtkWidget *w, GdkEventKey *e, struct tab *t)
{
	int			i;

	/* don't use w directly; use t->whatever instead */

	if (t == NULL)
		errx(1, "webview_keypress_cb");

	DNPRINTF(XT_D_KEY, "webview_keypress_cb: keyval 0x%x mask 0x%x t %p\n",
	    e->keyval, e->state, t);

	for (i = 0; i < LENGTH(keys); i++)
		if (e->keyval == keys[i].key && CLEAN(e->state) ==
		    keys[i].mask) {
			keys[i].func(t, &keys[i].arg);
			return (XT_CB_HANDLED);
		}

	return (XT_CB_PASSTHROUGH);
}

int
cmd_keypress_cb(GtkEntry *w, GdkEventKey *e, struct tab *t)
{
	int			rv = XT_CB_HANDLED;
	const gchar		*c = gtk_entry_get_text(w);

	if (t == NULL)
		errx(1, "cmd_keypress_cb");

	DNPRINTF(XT_D_CMD, "cmd_keypress_cb: keyval 0x%x mask 0x%x t %p\n",
	    e->keyval, e->state, t);

	/* sanity */
	if (c == NULL)
		e->keyval = GDK_Escape;
	else if (c[0] != ':')
		e->keyval = GDK_Escape;

	switch (e->keyval) {
	case GDK_BackSpace:
		if (strcmp(c, ":"))
			break;
		/* FALLTHROUGH */
	case GDK_Escape:
		gtk_widget_hide(t->cmd);
		gtk_widget_grab_focus(GTK_WIDGET(t->wv));
		goto done;
	}

	rv = XT_CB_PASSTHROUGH;
done:
	return (rv);
}

int
cmd_focusout_cb(GtkWidget *w, GdkEventFocus *e, struct tab *t)
{
	if (t == NULL)
		errx(1, "cmd_focusout_cb");

	DNPRINTF(XT_D_CMD, "cmd_focusout_cb: tab %d focus_wv %d\n",
	    t->tab_id, t->focus_wv);

	/* abort command when losing focus */
	gtk_widget_hide(t->cmd);
	if (t->focus_wv)
		gtk_widget_grab_focus(GTK_WIDGET(t->wv));
	else
		gtk_widget_grab_focus(GTK_WIDGET(t->uri_entry));

	return (XT_CB_PASSTHROUGH);
}

void
cmd_activate_cb(GtkEntry *entry, struct tab *t)
{
	int			i;
	char			*s;
	const gchar		*c = gtk_entry_get_text(entry);

	if (t == NULL)
		errx(1, "cmd_activate_cb");

	DNPRINTF(XT_D_CMD, "cmd_activate_cb: tab %d %s\n", t->tab_id, c);

	/* sanity */
	if (c == NULL)
		goto done;
	else if (c[0] != ':')
		goto done;
	if (strlen(c) < 2)
		goto done;
	s = (char *)&c[1];

	for (i = 0; i < LENGTH(cmds); i++)
		if (cmds[i].params) {
			if (!strncmp(s, cmds[i].cmd, strlen(cmds[i].cmd))) {
				cmds[i].arg.s = strdup(s);
				cmds[i].func(t, &cmds[i].arg);
			}
		} else {
			if (!strcmp(s, cmds[i].cmd))
				cmds[i].func(t, &cmds[i].arg);
		}

done:
	gtk_widget_hide(t->cmd);
}

GtkWidget *
create_browser(struct tab *t)
{
	GtkWidget		*w;

	if (t == NULL)
		errx(1, "create_browser");

	t->sb_h = GTK_SCROLLBAR(gtk_hscrollbar_new(NULL));
	t->sb_v = GTK_SCROLLBAR(gtk_vscrollbar_new(NULL));
	t->adjust_h = gtk_range_get_adjustment(GTK_RANGE(t->sb_h));
	t->adjust_v = gtk_range_get_adjustment(GTK_RANGE(t->sb_v));

	w = gtk_scrolled_window_new(t->adjust_h, t->adjust_v);
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w),
	    GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

	t->wv = WEBKIT_WEB_VIEW(webkit_web_view_new());
	gtk_container_add(GTK_CONTAINER(w), GTK_WIDGET(t->wv));

	g_signal_connect(t->wv, "notify::load-status",
	    G_CALLBACK(notify_load_status_cb), t);

	return (w);
}

GtkWidget *
create_window(void)
{
	GtkWidget		*w;

	w = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	gtk_window_set_default_size(GTK_WINDOW(w), 800, 600);
	gtk_widget_set_name(w, "xxxterm");
	gtk_window_set_wmclass(GTK_WINDOW(w), "xxxterm", "XXXTerm");

	return (w);
}

GtkWidget *
create_toolbar(struct tab *t)
{
	GtkWidget		*toolbar = gtk_toolbar_new();
	GtkToolItem		*i;

#if GTK_CHECK_VERSION(2,15,0)
	gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar),
	    GTK_ORIENTATION_HORIZONTAL);
#else
	gtk_toolbar_set_orientation(GTK_TOOLBAR(toolbar),
	    GTK_ORIENTATION_HORIZONTAL);
#endif
	gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_BOTH_HORIZ);

	i = gtk_tool_item_new();
	gtk_tool_item_set_expand(i, TRUE);
	t->uri_entry = gtk_entry_new();
	gtk_container_add(GTK_CONTAINER(i), t->uri_entry);
	g_signal_connect(G_OBJECT(t->uri_entry), "activate",
	    G_CALLBACK(activate_uri_entry_cb), t);
	gtk_toolbar_insert(GTK_TOOLBAR(toolbar), i, -1);

	return (toolbar);
}

void
delete_tab(struct tab *t)
{
	DNPRINTF(XT_D_TAB, "delete_tab: %p\n", t);

	if (t == NULL)
		return;

	TAILQ_REMOVE(&tabs, t, entry);
	if (TAILQ_EMPTY(&tabs))
		create_new_tab(NULL, 1);

	gtk_widget_destroy(t->vbox);
	g_free(t);
}

void
setup_webkit(struct tab *t)
{
	gchar			*strval;
	gchar			*ua;

	t->settings = webkit_web_settings_new();
	g_object_get((GObject *)t->settings, "user-agent", &strval, NULL);
	if (strval == NULL) {
		warnx("setup_webkit: can't get user-agent property");
		return;
	}

	if (asprintf(&ua, "%s %s+", strval, version) == -1)
		err(1, "aprintf user-agent");

	g_object_set((GObject *)t->settings,
	    "user-agent", ua, NULL);
	g_object_set((GObject *)t->settings,
	    "enable-scripts", enable_scripts, NULL);
	g_object_set((GObject *)t->settings,
	    "enable-plugins", enable_plugins, NULL);
	g_object_set((GObject *)t->settings,
	    "default-font-size", default_font_size, NULL);

	webkit_web_view_set_settings(t->wv, t->settings);

	g_free (strval);
	free(ua);
}

void
create_new_tab(char *title, int focus)
{
	struct tab		*t;
	int			load = 1;
	char			*newuri = NULL;

	DNPRINTF(XT_D_TAB, "create_new_tab: title %s focus %d\n", title, focus);

	if (tabless && !TAILQ_EMPTY(&tabs)) {
		DNPRINTF(XT_D_TAB, "create_new_tab: new tab rejected\n");
		return;
	}

	t = g_malloc0(sizeof *t);
	TAILQ_INSERT_TAIL(&tabs, t, entry);

	if (title == NULL) {
		title = "(untitled)";
		load = 0;
	} else {
		if (valid_url_type(title)) {
			newuri = guess_url_type(title);
			title = newuri;
		}
	}

	t->vbox = gtk_vbox_new(FALSE, 0);

	/* label for tab */
	t->label = gtk_label_new(title);
	gtk_widget_set_size_request(t->label, 100, -1);

	/* toolbar */
	t->toolbar = create_toolbar(t);
	gtk_box_pack_start(GTK_BOX(t->vbox), t->toolbar, FALSE, FALSE, 0);

	/* browser */
	t->browser_win = create_browser(t);
	gtk_box_pack_start(GTK_BOX(t->vbox), t->browser_win, TRUE, TRUE, 0);
	setup_webkit(t);

	/* command entry */
	t->cmd = gtk_entry_new();
	gtk_entry_set_inner_border(GTK_ENTRY(t->cmd), NULL);
	gtk_entry_set_has_frame(GTK_ENTRY(t->cmd), FALSE);
	gtk_box_pack_end(GTK_BOX(t->vbox), t->cmd, FALSE, FALSE, 0);

	/* and show it all */
	gtk_widget_show_all(t->vbox);
	t->tab_id = gtk_notebook_append_page(notebook, t->vbox,
	    t->label);

	g_object_connect((GObject*)t->cmd,
	    "signal::key-press-event", (GCallback)cmd_keypress_cb, t,
	    "signal::focus-out-event", (GCallback)cmd_focusout_cb, t,
	    "signal::activate", (GCallback)cmd_activate_cb, t,
	    NULL);

	g_object_connect((GObject*)t->wv,
	    "signal-after::key-press-event", (GCallback)webview_keypress_cb, t,
	    /* "signal::hovering-over-link", (GCallback)webview_hover_cb, t, */
	    "signal::download-requested", (GCallback)webview_download_cb, t,
	    "signal::mime-type-policy-decision-requested", (GCallback)webview_mimetype_cb, t,
	    "signal::navigation-policy-decision-requested", (GCallback)webview_npd_cb, t,
	    "signal::event", (GCallback)webview_event_cb, t,
	    NULL);

	/* hijack the unused keys as if we were the browser */
	g_object_connect((GObject*)t->toolbar,
	    "signal-after::key-press-event", (GCallback)webview_keypress_cb, t,
	    NULL);

	g_signal_connect(G_OBJECT(t->uri_entry), "focus",
	    G_CALLBACK(focus_uri_entry_cb), t);

	/* hide stuff */
	gtk_widget_hide(t->cmd);
	if (showurl == 0)
		gtk_widget_hide(t->toolbar);

	if (focus) {
		gtk_notebook_set_current_page(notebook, t->tab_id);
		DNPRINTF(XT_D_TAB, "create_new_tab: going to tab: %d\n",
		    t->tab_id);
	}

	if (load)
		webkit_web_view_load_uri(t->wv, title);
	else
		gtk_widget_grab_focus(GTK_WIDGET(t->uri_entry));

	if (newuri)
		free(newuri);
}

void
notebook_switchpage_cb(GtkNotebook *nb, GtkNotebookPage *nbp, guint pn,
    gpointer *udata)
{
	struct tab		*t;

	DNPRINTF(XT_D_TAB, "notebook_switchpage_cb: tab: %d\n", pn);

	TAILQ_FOREACH(t, &tabs, entry) {
		if (t->tab_id == pn) {
			DNPRINTF(XT_D_TAB, "notebook_switchpage_cb: going to "
			    "%d\n", pn);
			gtk_widget_hide(t->cmd);
		}
	}
}

void
create_canvas(void)
{
	GtkWidget		*vbox;
	
	vbox = gtk_vbox_new(FALSE, 0);
	notebook = GTK_NOTEBOOK(gtk_notebook_new());
	if (showtabs == 0)
		gtk_notebook_set_show_tabs(GTK_NOTEBOOK(notebook), FALSE);

	gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(notebook), TRUE, TRUE, 0);

	g_object_connect((GObject*)notebook,
	    "signal::switch-page", (GCallback)notebook_switchpage_cb, NULL,
	    NULL);

	main_window = create_window();
	gtk_container_add(GTK_CONTAINER(main_window), vbox);
	gtk_widget_show_all(main_window);
}

void
setup_cookies(void)
{
	if (cookiejar) {
		soup_session_remove_feature(session,
		    (SoupSessionFeature*)cookiejar);
		g_object_unref(cookiejar);
		cookiejar = NULL;
	}

	if (cookies_enabled == 0)
		return;

	cookiejar = soup_cookie_jar_text_new(cookie_file, read_only_cookies);
	soup_session_add_feature(session, (SoupSessionFeature*)cookiejar);
}

void
setup_proxy(char *uri)
{
	if (proxy_uri) {
		g_object_set(session, "proxy_uri", NULL, NULL);
		soup_uri_free(proxy_uri);
		proxy_uri = NULL;
	}
	if (http_proxy) {
		freepre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# Function calls in a single line.
#
# To run (on Linux):
#   $ ./translate_subx init.linux 0*.subx apps/subx-params.subx apps/calls.subx
#   $ mv a.elf apps/calls
#
# Example 1:
#   $ echo '(foo %eax)'                         |  apps/calls
#   # . (foo %eax)                                      # output has comments
#   ff 6/subop/push %eax                                # push
#   e8/call foo/disp32                                  # call
#   81 0/subop/add %esp 4/imm32                         # undo push
#
# Example 2:
#   $ echo '(foo Var1 *(eax + 4) "blah")'       |  apps/calls
#   # . (foo Var1 *(eax + 4) "blah")
#   68/push "blah"/imm32
#   ff 6/subop/push *(eax + 4)                          # push args in..
#   68/push Var1/imm32                                  # ..reverse order
#   e8/call foo/disp32
#   81 0/subop/add %esp 0xc/imm32                       # undo pushes
#
# Calls always begin with '(' as the first non-whitespace on a line.

== code

Entry:  # run tests if necessary, convert stdin if not
    # . prologue
    89/<- %ebp 4/r32/esp

    # initialize heap
    # . Heap = new-segment(Heap-size)
    # . . push args
    68/push Heap/imm32
    ff 6/subop/push *Heap-size
    # . . call
    e8/call new-segment/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32

    # - if argc > 1 and argv[1] == "test", then return run_tests()
    # if (argc <= 1) goto run-main
    81 7/subop/compare *ebp 1/imm32
    7e/jump-if-<= $subx-calls-main:interactive/disp8
    # if (!kernel-string-equal?(argv[1], "test")) goto run-main
    # . eax = kernel-string-equal?(argv[1], "test")
    # . . push args
    68/push "test"/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call kernel-string-equal?/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . if (eax == false) goto run-main
    3d/compare-eax-and 0/imm32/false
    74/jump-if-= $subx-calls-main:interactive/disp8
    # run-tests()
    e8/call run-tests/disp32
    # syscall(exit, *Num-test-failures)
    8b/-> *Num-test-failures 3/r32/ebx
    eb/jump $subx-calls-main:end/disp8
$subx-calls-main:interactive:
    # - otherwise convert stdin
    # subx-calls(Stdin, Stdout)
    # . . push args
    68/push Stdout/imm32
    68/push Stdin/imm32
    # . . call
    e8/call subx-calls/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # syscall(exit, 0)
    bb/copy-to-ebx 0/imm32
$subx-calls-main:end:
    e8/call syscall_exit/disp32

subx-calls:  # in: (addr buffered-file), out: (addr buffered-file)
    # pseudocode:
    #   var line: (stream byte 512)
    #   var words: (stream slice 16)  # at most function name and 15 args
    #   while true
    #     clear-stream(line)
    #     read-line-buffered(in, line)
    #     if (line->write == 0) break                           # end of file
    #     skip-chars-matching(line, ' ')
    #     if line->data[line->read] != '('
    #       write-stream-data(out, line)
    #       continue
    #     # emit comment
    #     write-buffered(out, "# . ")
    #     write-stream-data(out, line)
    #     # emit code
    #     ++line->read to skip '('
    #     clear-stream(words)
    #     words = parse-line(line)
    #     emit-call(out, words)
    #   flush(out)
    #
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    56/push-esi
    # var line/esi: (stream byte 512)
    81 5/subop/subtract %esp 0x200/imm32
    68/push 0x200/imm32/length
    68/push 0/imm32/read
    68/push 0/imm32/write
    89/<- %esi 4/r32/esp
    # var words/edx: (stream slice 128)  # 16 rows * 8 bytes/row
    81 5/subop/subtract %esp 0x80/imm32
    68/push 0x80/imm32/length
    68/push 0/imm32/read
    68/push 0/imm32/write
    89/<- %edx 4/r32/esp
$subx-calls:loop:
    # clear-stream(line)
    # . . push args
    56/push-esi
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # read-line-buffered(in, line)
    # . . push args
    56/push-esi
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call read-line-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
$subx-calls:check0:
    # if (line->write == 0) break
    81 7/subop/compare *esi 0/imm32
    0f 84/jump-if-= $subx-calls:break/disp32
    # skip-chars-matching(line, ' ')
    # . . push args
    68/push 0x20/imm32/space
    56/push-esi
    # . . call
    e8/call skip-chars-matching/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # if (line->data[line->read] == '(') goto convert-call
    # . ecx = line->read
    8b/-> *(esi+4) 1/r32/ecx
    # . eax = line->data[line->read]
    31/xor-with %eax 0/r32/eax
    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
    # . if (eax == '(') goto convert-call
    3d/compare-eax-and 0x28/imm32/open-paren
    74/jump-if-= $subx-calls:convert-call/disp8
$subx-calls:pass-through:
    # write-stream-data(out, line)
    # . . push args
    56/push-esi
    ff 6/subop/push *(ebp+0xc)
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # continue
    eb/jump $subx-calls:loop/disp8
$subx-calls:convert-call:
    # - emit comment
    # write-buffered(out, "# . ")
    # . . push args
    68/push "# . "/imm32
    ff 6/subop/push *(ebp+0xc)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-stream-data(out, line)
    # . . push args
    56/push-esi
    ff 6/subop/push *(ebp+0xc)
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # - emit code
    # ++line->read to skip '('
    ff 0/subop/increment *(esi+4)
    # clear-stream(words)
    # . . push args
    52/push-edx
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # words = parse-line(line)
    # . . push args
    52/push-edx
    56/push-esi
    # . . call
    e8/call parse-line/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # emit-call(out, words)
    # . . push args
    52/push-edx
    ff 6/subop/push *(ebp+0xc)
    # . . call
    e8/call emit-call/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # loop
    e9/jump $subx-calls:loop/disp32
$subx-calls:break:
    # flush(out)
    # . . push args
    ff 6/subop/push *(ebp+0xc)
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
$subx-calls:end:
    # . reclaim locals
    81 0/subop/add %esp 0x298/imm32  # 0x20c + 0x8c
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

parse-line:  # line: (addr stream byte), words: (addr stream slice)
    # pseudocode:
    #   var word-slice: slice
    #   while true
    #     word-slice = next-word-string-or-expression-without-metadata(line)
    #     if slice-empty?(word-slice)
    #       break                                 # end of line
    #     write-int(words, word-slice->start)
    #     write-int(words, word-slice->end)
    #
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    51/push-ecx
    # var word-slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
$parse-line:loop:
    # word-slice = next-word-string-or-expression-without-metadata(line)
    # . . push args
    51/push-ecx
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
$parse-line:check1:
    # if (slice-empty?(word-slice)) break
    # . eax = slice-empty?(word-slice)
    # . . push args
    51/push-ecx
    # . . call
    e8/call slice-empty?/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . if (eax != false) break
    3d/compare-eax-and 0/imm32/false
    0f 85/jump-if-!= $parse-line:end/disp32
#?     # dump word-slice {{{
#?     # . write(2/stderr, "w: ")
#?     # . . push args
#?     68/push "w: "/imm32
#?     68/push 2/imm32/stderr
#?     # . . call
#?     e8/call write/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 8/imm32
#?     # . clear-stream($Stderr->buffer)
#?     # . . push args
#?     68/push  $Stderr->buffer/imm32
#?     # . . call
#?     e8/call clear-stream/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 4/imm32
#?     # . write-slice-buffered(Stderr, word-slice)
#?     # . . push args
#?     51/push-ecx
#?     68/push Stderr/imm32
#?     # . . call
#?     e8/call write-slice-buffered/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 8/imm32
#?     # . flush(Stderr)
#?     # . . push args
#?     68/push Stderr/imm32
#?     # . . call
#?     e8/call flush/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 4/imm32
#?     # . write(2/stderr, "$\n")
#?     # . . push args
#?     68/push "$\n"/imm32
#?     68/push 2/imm32/stderr
#?     # . . call
#?     e8/call write/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 8/imm32
#?     # }}}
$parse-line:write-word:
    # write-int(words, word-slice->start)
    # . . push args
    ff 6/subop/push *ecx
    ff 6/subop/push *(ebp+0xc)
    # . . call
    e8/call write-int/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-int(words, word-slice->end)
    # . . push args
    ff 6/subop/push *(ecx+4)
    ff 6/subop/push *(ebp+0xc)
    # . . call
    e8/call write-int/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # loop
    e9/jump $parse-line:loop/disp32
$parse-line:end:
    # . reclaim locals
    81 0/subop/add %esp 8/imm32
    # . restore registers
    59/pop-to-ecx
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

emit-call:  # out: (addr buffered-file), words: (addr stream slice)
    # pseudocode:
    #   if (words->write < 8) abort
    #   curr = &words->data[words->write-8]
    #   min = words->data
    #   # emit pushes
    #   while true
    #     if (curr <= min) break
    #     if *curr->start in '%' '*'
    #       write-buffered(out, "ff 6/subop/push ")
    #       write-slice-buffered(out, curr)
    #       write-buffered(out, "\n")
    #     else
    #       write-buffered(out, "68/push ")
    #       write-slice-buffered(out, curr)
    #       write-buffered(out, "/imm32\n")
    #     curr -= 8
    #   # emit call
    #   write-buffered(out, "e8/call ")
    #   write-slice-buffered(out, curr)
    #   write-buffered(out, "/disp32\n")
    #   # emit pops
    #   write-buffered(out, "81 0/subop/add %esp ")
    #   write-int32-hex-buffered(out, words->write >> 1 - 4)
    #   write-buffered(out, "/imm32\n")
    #
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    52/push-edx
    56/push-esi
    # esi = words
    8b/-> *(ebp+0xc) 6/r32/esi
    # if (words->write < 8) abort
    # . ecx = words->write - 8
    8b/-> *esi 1/r32/ecx
    81 5/subop/subtract %ecx 8/imm32
    0f 8c/jump-if-< $emit-call:error1/disp32
    # var curr/ecx: (addr slice) = &words->data[words->write-8]
    8d/copy-address *(esi+ecx+0xc) 1/r32/ecx
    # var min/edx: (addr byte) = words->data
    8d/copy-address *(esi+0xc) 2/r32/edx
    # - emit pushes
$emit-call:push-loop:
    # if (curr <= min) break
    39/compare %ecx 2/r32/edx
    0f 8e/jump-if-<= $emit-call:call-instruction/disp32
    # if (*curr->start in '%' '*') goto push-rm32
    # . var start/eax: (addr byte) = curr->start
    8b/-> *ecx 0/r32/eax
    # . var c/eax: byte = *eax
    8b/-> *eax 0/r32/eax
    81 4/subop/and %eax 0xff/imm32
    # . if (c == '%') goto push-rm32
    3d/compare-eax-and 0x25/imm32/percent
    74/jump-if-= $emit-call:push-rm32/disp8
    # . if (c == '*') goto push-rm32
    3d/compare-eax-and 0x2a/imm32/asterisk
    74/jump-if-= $emit-call:push-rm32/disp8
$emit-call:push-imm32:
    # write-buffered(out, "68/push ")
    68/push "68/push "/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-slice-buffered(out, curr)
    # . . push args
    51/push-ecx
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-slice-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-buffered(out, "/imm32\n")
    68/push "/imm32\n"/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # continue
    eb/jump $emit-call:next-push/disp8
$emit-call:push-rm32:
    # write-buffered(out, "ff 6/subop/push ")
    # . . push args
    68/push "ff 6/subop/push "/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-slice-buffered(out, curr)
    # . . push args
    51/push-ecx
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-slice-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-buffered(out, "\n")
    68/push Newline/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
$emit-call:next-push:
    # curr -= 8
    81 5/subop/subtract %ecx 8/imm32
    # loop
    e9/jump $emit-call:push-loop/disp32
$emit-call:call-instruction:
    # write-buffered(out, "e8/call ")
    68/push "e8/call "/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-slice-buffered(out, curr)
    # . . push args
    51/push-ecx
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-slice-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-buffered(out, "/disp32\n")
    68/push "/disp32\n"/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
$emit-call:pop-instruction:
    # write-buffered(out, "81 0/subop/add %esp ")
    68/push "81 0/subop/add %esp "/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-int32-hex-buffered(out, words->write >> 1 - 4)
    # . . push args
    8b/-> *esi 0/r32/eax
    c1/shift 7/subop/arith-right %eax 1/imm8
    2d/subtract-from-eax 4/imm32
    50/push-eax
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-int32-hex-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # write-buffered(out, "/imm32\n")
    68/push "/imm32\n"/imm32
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
$emit-call:end:
    # . restore registers
    5e/pop-to-esi
    5a/pop-to-edx
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

$emit-call:error1:
    # print(stderr, "error: calls.subx: '()' is not a valid call")
    # . write-buffered(Stderr, "error: calls.subx: '()' is not a valid call")
    # . . push args
    68/push "error: calls.subx: '()' is not a valid call"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . flush(Stderr)
    # . . push args
    68/push Stderr/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . syscall(exit, 1)
    bb/copy-to-ebx 1/imm32
    e8/call syscall_exit/disp32
    # never gets here

test-subx-calls-passes-most-lines-through:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . clear-stream($_test-input-buffered-file->buffer)
    # . . push args
    68/push  $_test-input-buffered-file->buffer/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . clear-stream(_test-output-stream)
    # . . push args
    68/push _test-output-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . clear-stream($_test-output-buffered-file->buffer)
    # . . push args
    68/push  $_test-output-buffered-file->buffer/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . write(_test-input-stream, "== abcd 0x1\n")
    # . . push args
    68/push "== abcd 0x1\n"/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # subx-calls(_test-input-buffered-file, _test-output-buffered-file)
    # . . push args
    68/push _test-output-buffered-file/imm32
    68/push _test-input-buffered-file/imm32
    # . . call
    e8/call subx-calls/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check that the line just passed through
    # . flush(_test-output-buffered-file)
    # . . push args
    68/push _test-output-buffered-file/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . check-stream-equal(_test-output-stream, "== abcd 0x1\n", msg)
    # . . push args
    68/push "F - test-subx-calls-passes-most-lines-through"/imm32
    68/push "== abcd 0x1\n"/imm32
    68/push _test-output-stream/imm32
    # . . call
    e8/call check-stream-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-subx-calls-processes-calls:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . clear-stream($_test-input-buffered-file->buffer)
    # . . push args
    68/push  $_test-input-buffered-file->buffer/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . clear-stream(_test-output-stream)
    # . . push args
    68/push _test-output-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . clear-stream($_test-output-buffered-file->buffer)
    # . . push args
    68/push  $_test-output-buffered-file->buffer/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . write(_test-input-stream, "(foo %eax)\n")
    # . . push args
    68/push "(foo %eax)\n"/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # subx-calls(_test-input-buffered-file, _test-output-buffered-file)
    # . . push args
    68/push _test-output-buffered-file/imm32
    68/push _test-input-buffered-file/imm32
    # . . call
    e8/call subx-calls/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check that the line just passed through
    # . flush(_test-output-buffered-file)
    # . . push args
    68/push _test-output-buffered-file/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
#?     # dump _test-output-stream {{{
#?     # . write(2/stderr, "^")
#?     # . . push args
#?     68/push "^"/imm32
#?     68/push 2/imm32/stderr
#?     # . . call
#?     e8/call write/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 8/imm32
#?     # . write-stream(2/stderr, _test-output-stream)
#?     # . . push args
#?     68/push _test-output-stream/imm32
#?     68/push 2/imm32/stderr
#?     # . . call
#?     e8/call write-stream/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 8/imm32
#?     # . write(2/stderr, "$\n")
#?     # . . push args
#?     68/push "$\n"/imm32
#?     68/push 2/imm32/stderr
#?     # . . call
#?     e8/call write/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 8/imm32
#?     # . rewind-stream(_test-output-stream)
#?     # . . push args
#?     68/push _test-output-stream/imm32
#?     # . . call
#?     e8/call rewind-stream/disp32
#?     # . . discard args
#?     81 0/subop/add %esp 4/imm32
#?     # }}}
    # . check-next-stream-line-equal(_test-output-stream, "# . (foo %eax)", msg)
    # . . push args
    68/push "F - test-subx-calls-processes-calls: comment"/imm32
    68/push "# . (foo %eax)"/imm32
    68/push _test-output-stream/imm32
    # . . call
    e8/call check-next-stream-line-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . check-next-stream-line-equal(_test-output-stream, "ff 6/subop/push %eax", msg)
    # . . push args
    68/push "F - test-subx-calls-processes-calls: arg 0"/imm32
    68/push "ff 6/subop/push %eax"/imm32
    68/push _test-output-stream/imm32
    # . . call
    e8/call check-next-stream-line-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . check-next-stream-line-equal(_test-output-stream, "e8/call foo/disp32", msg)
    # . . push args
    68/push "F - test-subx-calls-processes-calls: call"/imm32
    68/push "e8/call foo/disp32"/imm32
    68/push _test-output-stream/imm32
    # . . call
    e8/call check-next-stream-line-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . check-next-stream-line-equal(_test-output-stream, "81 0/subop/add %esp 4/imm32", msg)
    # . . push args
    68/push "F - test-subx-calls-processes-calls: pops"/imm32
    68/push "81 0/subop/add %esp 0x00000004/imm32"/imm32
    68/push _test-output-stream/imm32
    # . . call
    e8/call check-next-stream-line-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

next-word-string-or-expression-without-metadata:  # line: (addr stream byte), out: (addr slice)
    # pseudocode:
    #   skip-chars-matching(line, ' ')
    #   if line->read >= line->write              # end of line
    #     out = {0, 0}
    #     return
    #   out->start = &line->data[line->read]
    #   if line->data[line->read] == '#'          # comment
    #     out->end = &line->data[line->write]     # skip to end of line
    #     return
    #   if line->data[line->read] == '"'          # string literal
    #     skip-string(line)
    #     out->end = &line->data[line->read]      # no metadata
    #     return
    #   if line->data[line->read] == '*'          # expression
    #     if line->data[line->read + 1] == ' '
    #       abort
    #     if line->data[line->read + 1] == '('
    #       skip-until-close-paren(line)
    #       if (line->data[line->read] != ')'
    #         abort
    #       ++line->read to skip ')'
    #     out->end = &line->data[line->read]
    #     return
    #   if line->data[line->read] == ')'
    #     ++line->read to skip ')'
    #     # make sure there's nothing else of importance
    #     if line->read >= line->write
    #       out = {0, 0}
    #       return
    #     if line->data[line->read] != ' '
    #       abort
    #     skip-chars-matching-whitespace(line)
    #     if line->read >= line->write
    #       out = {0, 0}
    #       return
    #     if line->data[line->read] == '#'        # only thing permitted after ')' is a comment
    #       out = {0, 0}
    #       return
    #     abort
    #   # default case: read a word -- but no metadata
    #   while true
    #     if line->read >= line->write
    #       break
    #     if line->data[line->read] == ' '
    #       break
    #     if line->data[line->read] == ')'
    #       break
    #     if line->data[line->read] == '/'
    #       abort
    #     ++line->read
    #   out->end = &line->data[line->read]
    #
    # registers:
    #   ecx: often line->read
    #   eax: often line->data[line->read]
    #
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # . save registers
    50/push-eax
    51/push-ecx
    56/push-esi
    57/push-edi
    # esi = line
    8b/-> *(ebp+8) 6/r32/esi
    # edi = out
    8b/-> *(ebp+0xc) 7/r32/edi
    # skip-chars-matching(line, ' ')
    # . . push args
    68/push 0x20/imm32/space
    ff 6/subop/push *(ebp+8)
    # . . call
    e8/call skip-chars-matching/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
$next-word-string-or-expression-without-metadata:check0:
    # if (line->read >= line->write) abort because we didn't encounter a final ')'
    # . ecx = line->read
    8b/-> *(esi+4) 1/r32/ecx
    # . if (ecx >= line->write) abort
    3b/compare<- *esi 1/r32/ecx
    0f 8d/jump-if->= $next-word-string-or-expression-without-metadata:error0/disp32
$next-word-string-or-expression-without-metadata:check-for-comment:
    # out->start = &line->data[line->read]
    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
    89/<- *edi 0/r32/eax
    # if (line->data[line->read] != '#') goto next check
    # . var eax: byte = line->data[line->read]
    31/xor-with %eax 0/r32/eax
    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
    # . if (eax != '#') goto next check
    3d/compare-eax-and 0x23/imm32/pound
    75/jump-if-!= $next-word-string-or-expression-without-metadata:check-for-string-literal/disp8
$next-word-string-or-expression-without-metadata:comment:
    # out->end = &line->data[line->write]
    8b/-> *esi 0/r32/eax
    8d/copy-address *(esi+eax+0xc) 0/r32/eax
    89/<- *(edi+4) 0/r32/eax
    # line->read = line->write  # skip rest of line
    8b/-> *esi 0/r32/eax
    89/<- *(esi+4) 0/r32/eax
    # return
    e9/jump $next-word-string-or-expression-without-metadata:end/disp32
$next-word-string-or-expression-without-metadata:check-for-string-literal:
    # if (line->data[line->read] != '"') goto next check
    3d/compare-eax-and 0x22/imm32/dquote
    75/jump-if-!= $next-word-string-or-expression-without-metadata:check-for-expression/disp8
$next-word-string-or-expression-without-metadata:string-literal:
    # skip-string(line)
    # . . push args
    56/push-esi
    # . . call
    e8/call skip-string/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # out->end = &line->data[line->read]
    8b/-> *(esi+4) 1/r32/ecx
    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
    89/<- *(edi+4) 0/r32/eax
    # return
    e9/jump $next-word-string-or-expression-without-metadata:end/disp32
$next-word-string-or-expression-without-metadata:check-for-expression:
    # if (line->data[line->read] != '*') goto next check
    3d/compare-eax-and 0x2a/imm32/asterisk
    75/jump-if-!= $next-word-string-or-expression-without-metadata:check-for-end-of-call/disp8
    # if (line->data[line->read + 1] == ' ') goto error1
    8a/copy-byte *(esi+ecx+0xd) 0/r32/AL
    3d/compare-eax-and 0x20/imm32/space
    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:error1/disp32
    # if (line->data[line->read + 1] != '(') goto regular-word
    3d/compare-eax-and 0x28/imm32/open-paren
    0f 85/jump-if-!= $next-word-string-or-expression-without-metadata:regular-word-without-metadata/disp32
$next-word-string-or-expression-without-metadata:paren:
    # skip-until-close-paren(line)
    # . . push args
    56/push-esi
    # . . call
    e8/call skip-until-close-paren/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # if (line->data[line->read] != ')') goto error2
    # . eax = line->data[line->read]
    8b/-> *(esi+4) 1/r32/ecx
    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
    # . if (eax != ')') goto error2
    3d/compare-eax-and 0x29/imm32/close-paren
    0f 85/jump-if-!= $next-word-string-or-expression-without-metadata:error2/disp32
    # ++line->read to skip ')'
    ff 0/subop/increment *(esi+4)
    # out->end = &line->data[line->read]
    8b/-> *(esi+4) 1/r32/ecx
    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
    89/<- *(edi+4) 0/r32/eax
    # return
    e9/jump $next-word-string-or-expression-without-metadata:end/disp32
$next-word-string-or-expression-without-metadata:check-for-end-of-call:
    # if (line->data[line->read] != ')') goto next check
    3d/compare-eax-and 0x29/imm32/close-paren
    75/jump-if-!= $next-word-string-or-expression-without-metadata:regular-word-without-metadata/disp8
    # ++line->read to skip ')'
    ff 0/subop/increment *(esi+4)
    # - error checking: make sure there's nothing else of importance on the line
    # if (line->read >= line->write) return out = {0, 0}
    # . ecx = line->read
    8b/-> *(esi+4) 1/r32/ecx
    # . if (ecx >= line->write) return {0, 0}
    3b/compare<- *esi 1/r32/ecx
    0f 8d/jump-if->= $next-word-string-or-expression-without-metadata:return-eol/disp32
    # if (line->data[line->read] == '/') goto error3
    # . eax = line->data[line->read]
    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
    # . if (eax == '/') goto error3
    3d/compare-eax-and 0x2f/imm32/slash
    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:error3/disp32
    # skip-chars-matching-whitespace(line)
    # . . push args
    56/push-esi
    # . . call
    e8/call skip-chars-matching-whitespace/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # if (line->read >= line->write) return out = {0, 0}
    # . ecx = line->read
    8b/-> *(esi+4) 1/r32/ecx
    # . if (ecx >= line->write) return {0, 0}
    3b/compare<- *esi 1/r32/ecx
    0f 8d/jump-if->= $next-word-string-or-expression-without-metadata:return-eol/disp32
    # if (line->data[line->read] == '#') return out = {0, 0}
    # . eax = line->data[line->read]
    8b/-> *(esi+4) 1/r32/ecx
    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
    # . if (eax == '#') return out = {0, 0}
    3d/compare-eax-and 0x23/imm32/pound
    74/jump-if-= $next-word-string-or-expression-without-metadata:return-eol/disp8
    # otherwise goto error4
    e9/jump $next-word-string-or-expression-without-metadata:error4/disp32
$next-word-string-or-expression-without-metadata:regular-word-without-metadata:
    # if (line->read >= line->write) break
    # . ecx = line->read
    8b/-> *(esi+4) 1/r32/ecx
    # . if (ecx >= line->write) break
    3b/compare<- *esi 1/r32/ecx
    7d/jump-if->= $next-word-string-or-expression-without-metadata:regular-word-break/disp8
    # if (line->data[line->read] == ' ') break
    # . eax = line->data[line->read]
    8b/-> *(esi+4) 1/r32/ecx
    8a/copy-byte *(esi+ecx+0xc) 0/r32/AL
    # . if (eax == ' ') break
    3d/compare-eax-and 0x20/imm32/space
    74/jump-if-= $next-word-string-or-expression-without-metadata:regular-word-break/disp8
    # if (line->data[line->read] == ')') break
    3d/compare-eax-and 0x29/imm32/close-paren
    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:regular-word-break/disp32
    # if (line->data[line->read] == '/') goto error5
    3d/compare-eax-and 0x2f/imm32/slash
    0f 84/jump-if-= $next-word-string-or-expression-without-metadata:error5/disp32
    # ++line->read
    ff 0/subop/increment *(esi+4)
    # loop
    e9/jump $next-word-string-or-expression-without-metadata:regular-word-without-metadata/disp32
$next-word-string-or-expression-without-metadata:regular-word-break:
    # out->end = &line->data[line->read]
    8b/-> *(esi+4) 1/r32/ecx
    8d/copy-address *(esi+ecx+0xc) 0/r32/eax
    89/<- *(edi+4) 0/r32/eax
    eb/jump $next-word-string-or-expression-without-metadata:end/disp8
$next-word-string-or-expression-without-metadata:return-eol:
    # return out = {0, 0}
    c7 0/subop/copy *edi 0/imm32
    c7 0/subop/copy *(edi+4) 0/imm32
$next-word-string-or-expression-without-metadata:end:
    # . restore registers
    5f/pop-to-edi
    5e/pop-to-esi
    59/pop-to-ecx
    58/pop-to-eax
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

$next-word-string-or-expression-without-metadata:error0:
    # print(stderr, "error: missing final ')' in '" line "'")
    # . write-buffered(Stderr, "error: missing final ')' in '")
    # . . push args
    68/push "error: missing final ')' in '"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-stream-data(Stderr, line)
    # . . push args
    56/push-esi
    68/push Stderr/imm32
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-buffered(Stderr, "'")
    # . . push args
    68/push "'"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . flush(Stderr)
    # . . push args
    68/push Stderr/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . syscall(exit, 1)
    bb/copy-to-ebx 1/imm32
    e8/call  syscall_exit/disp32
    # never gets here

$next-word-string-or-expression-without-metadata:error1:
    # print(stderr, "error: no space allowed after '*' in '" line "'")
    # . write-buffered(Stderr, "error: no space allowed after '*' in '")
    # . . push args
    68/push "error: no space allowed after '*' in '"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-stream-data(Stderr, line)
    # . . push args
    56/push-esi
    68/push Stderr/imm32
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-buffered(Stderr, "'")
    # . . push args
    68/push "'"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . flush(Stderr)
    # . . push args
    68/push Stderr/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . syscall(exit, 1)
    bb/copy-to-ebx 1/imm32
    e8/call syscall_exit/disp32
    # never gets here

$next-word-string-or-expression-without-metadata:error2:
    # print(stderr, "error: *(...) expression must be all on a single line in '" line "'")
    # . write-buffered(Stderr, "error: *(...) expression must be all on a single line in '")
    # . . push args
    68/push "error: *(...) expression must be all on a single line in '"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-stream-data(Stderr, line)
    # . . push args
    56/push-esi
    68/push Stderr/imm32
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-buffered(Stderr, "'")
    # . . push args
    68/push "'"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . flush(Stderr)
    # . . push args
    68/push Stderr/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . syscall(exit, 1)
    bb/copy-to-ebx 1/imm32
    e8/call syscall_exit/disp32
    # never gets here

$next-word-string-or-expression-without-metadata:error3:
    # print(stderr, "error: no metadata after calls; just use a comment (in '" line "')")
    # . write-buffered(Stderr, "error: no metadata after calls; just use a comment (in '")
    # . . push args
    68/push "error: no metadata after calls; just use a comment (in '"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-stream-data(Stderr, line)
    # . . push args
    56/push-esi
    68/push Stderr/imm32
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-buffered(Stderr, "')")
    # . . push args
    68/push "')"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . flush(Stderr)
    # . . push args
    68/push Stderr/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . syscall(exit, 1)
    bb/copy-to-ebx 1/imm32
    e8/call syscall_exit/disp32
    # never gets here

$next-word-string-or-expression-without-metadata:error4:
    # print(stderr, "error: unexpected text after end of call in '" line "'")
    # . write-buffered(Stderr, "error: unexpected text after end of call in '")
    # . . push args
    68/push "error: unexpected text after end of call in '"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-stream-data(Stderr, line)
    # . . push args
    56/push-esi
    68/push Stderr/imm32
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-buffered(Stderr, "'")
    # . . push args
    68/push "'"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . flush(Stderr)
    # . . push args
    68/push Stderr/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . syscall(exit, 1)
    bb/copy-to-ebx 1/imm32
    e8/call syscall_exit/disp32
    # never gets here

$next-word-string-or-expression-without-metadata:error5:
    # print(stderr, "error: no metadata anywhere in calls (in '" line "')")
    # . write-buffered(Stderr, "error: no metadata anywhere in calls (in '")
    # . . push args
    68/push "error: no metadata anywhere in calls (in '"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-stream-data(Stderr, line)
    # . . push args
    56/push-esi
    68/push Stderr/imm32
    # . . call
    e8/call write-stream-data/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . write-buffered(Stderr, "')")
    # . . push args
    68/push "')"/imm32
    68/push Stderr/imm32
    # . . call
    e8/call write-buffered/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # . flush(Stderr)
    # . . push args
    68/push Stderr/imm32
    # . . call
    e8/call flush/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # . syscall(exit, 1)
    bb/copy-to-ebx 1/imm32
    e8/call syscall_exit/disp32
    # never gets here

test-next-word-string-or-expression-without-metadata:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, "  ab")
    # . . push args
    68/push "  ab"/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(_test-input-stream->read, 4, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata/updates-stream-read-correctly"/imm32
    68/push 4/imm32
    b8/copy-to-eax _test-input-stream/imm32
    ff 6/subop/push *(eax+4)
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->start - _test-input-stream->data, 2, msg)
    # . check-ints-equal(slice->start - _test-input-stream, 14, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata: start"/imm32
    68/push 0xe/imm32
    # . . push slice->start - _test-input-stream
    8b/-> *ecx 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end - _test-input-stream->data, 4, msg)
    # . check-ints-equal(slice->end - _test-input-stream, 16, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata: end"/imm32
    68/push 0x10/imm32
    # . . push slice->end - _test-input-stream
    8b/-> *(ecx+4) 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-returns-whole-comment:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, "  # a")
    # . . push args
    68/push "  # a"/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(_test-input-stream->read, 5, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-comment/updates-stream-read-correctly"/imm32
    68/push 5/imm32
    b8/copy-to-eax _test-input-stream/imm32
    ff 6/subop/push *(eax+4)
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->start - _test-input-stream->data, 2, msg)
    # . check-ints-equal(slice->start - _test-input-stream, 14, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-comment: start"/imm32
    68/push 0xe/imm32
    # . . push slice->start - _test-input-stream
    8b/-> *ecx 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end - _test-input-stream->data, 5, msg)
    # . check-ints-equal(slice->end - _test-input-stream, 17, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-comment: end"/imm32
    68/push 0x11/imm32
    # . . push slice->end - _test-input-stream
    8b/-> *(ecx+4) 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-returns-string-literal:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, " \"a b\" ")
    # . . push args
    68/push " \"a b\" "/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-literal: start"/imm32
    68/push 0xd/imm32
    # . . push slice->start - _test-input-stream
    8b/-> *ecx 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end - _test-input-stream->data, 6, msg)
    # . check-ints-equal(slice->end - _test-input-stream, 18, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-literal: end"/imm32
    68/push 0x12/imm32
    # . . push slice->end - _test-input-stream
    8b/-> *(ecx+4) 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-returns-string-with-escapes:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, " \"a\\\"b\"")
    # . . push args
    68/push " \"a\\\"b\""/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-with-escapes: start"/imm32
    68/push 0xd/imm32
    # . . push slice->start - _test-input-stream
    8b/-> *ecx 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end - _test-input-stream->data, 7, msg)
    # . check-ints-equal(slice->end - _test-input-stream, 19, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-string-with-escapes: end"/imm32
    68/push 0x13/imm32
    # . . push slice->end - _test-input-stream
    8b/-> *(ecx+4) 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-returns-whole-expression:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, " *(a b) ")
    # . . push args
    68/push " *(a b) "/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-expression: start"/imm32
    68/push 0xd/imm32
    # . . push slice->start - _test-input-stream
    8b/-> *ecx 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end - _test-input-stream->data, 7, msg)
    # . check-ints-equal(slice->end - _test-input-stream, 19, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-whole-expression: end"/imm32
    68/push 0x13/imm32
    # . . push slice->end - _test-input-stream
    8b/-> *(ecx+4) 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-returns-eol-on-trailing-close-paren:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, " ) ")
    # . . push args
    68/push " ) "/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(slice->start, 0, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-eol-on-trailing-close-paren: start"/imm32
    68/push 0/imm32
    ff 6/subop/push *ecx
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end, 0, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-returns-eol-on-trailing-close-paren: end"/imm32
    68/push 0/imm32
    ff 6/subop/push *(ecx+4)
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-handles-comment-after-trailing-close-paren:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, " ) # abc ")
    # . . push args
    68/push " ) # abc "/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(slice->start, 0, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-handles-comment-after-trailing-close-paren: start"/imm32
    68/push 0/imm32
    ff 6/subop/push *ecx
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end, 0, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-handles-comment-after-trailing-close-paren: end"/imm32
    68/push 0/imm32
    ff 6/subop/push *(ecx+4)
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-handles-newline-after-trailing-close-paren:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, " )\n")
    # . . push args
    68/push " )\n"/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(slice->start, 0, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-handles-newline-after-trailing-close-paren: start"/imm32
    68/push 0/imm32
    ff 6/subop/push *ecx
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end, 0, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-handles-newline-after-trailing-close-paren: end"/imm32
    68/push 0/imm32
    ff 6/subop/push *(ecx+4)
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return

test-next-word-string-or-expression-without-metadata-stops-at-close-paren:
    # . prologue
    55/push-ebp
    89/<- %ebp 4/r32/esp
    # setup
    # . clear-stream(_test-input-stream)
    # . . push args
    68/push _test-input-stream/imm32
    # . . call
    e8/call clear-stream/disp32
    # . . discard args
    81 0/subop/add %esp 4/imm32
    # var slice/ecx: slice
    68/push 0/imm32/end
    68/push 0/imm32/start
    89/<- %ecx 4/r32/esp
    # write(_test-input-stream, " abc) # def")
    # . . push args
    68/push " abc) # def"/imm32
    68/push _test-input-stream/imm32
    # . . call
    e8/call write/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # next-word-string-or-expression-without-metadata(_test-input-stream, slice)
    # . . push args
    51/push-ecx
    68/push _test-input-stream/imm32
    # . . call
    e8/call next-word-string-or-expression-without-metadata/disp32
    # . . discard args
    81 0/subop/add %esp 8/imm32
    # check-ints-equal(slice->start - _test-input-stream->data, 1, msg)
    # . check-ints-equal(slice->start - _test-input-stream, 13, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-stops-at-close-paren: start"/imm32
    68/push 0xd/imm32
    # . . push slice->start - _test-input-stream
    8b/-> *ecx 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # check-ints-equal(slice->end - _test-input-stream->data, 4, msg)
    # . check-ints-equal(slice->end - _test-input-stream, 16, msg)
    # . . push args
    68/push "F - test-next-word-string-or-expression-without-metadata-stops-at-close-paren: end"/imm32
    68/push 0x10/imm32
    # . . push slice->end - _test-input-stream
    8b/-> *(ecx+4) 0/r32/eax
    81 5/subop/subtract %eax _test-input-stream/imm32
    50/push-eax
    # . . call
    e8/call check-ints-equal/disp32
    # . . discard args
    81 0/subop/add %esp 0xc/imm32
    # . epilogue
    89/<- %esp 5/r32/ebp
    5d/pop-to-ebp
    c3/return